Merge branch 'release/os/4.5' into os_4.5-feature_checkpoint_table_improvements-merge

# Conflicts:
#	node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt
This commit is contained in:
Kyriakos Tharrouniatis 2020-03-19 21:49:55 +00:00
commit da320514a5
120 changed files with 3781 additions and 1091 deletions

View File

@ -635,67 +635,83 @@ buildScan {
termsOfServiceAgree = 'yes'
}
ext.generalPurpose = [
numberOfShards: 10,
streamOutput: false,
coresPerFork: 2,
memoryInGbPerFork: 12,
nodeTaints: "small"
]
ext.largeScaleSet = [
numberOfShards: 15,
streamOutput: false,
coresPerFork: 6,
memoryInGbPerFork: 10,
nodeTaints: "big"
]
task allParallelIntegrationTest(type: ParallelTestGroup) {
dependsOn dependxiesModule
podLogLevel PodLogLevel.INFO
testGroups "integrationTest"
numberOfShards 10
streamOutput false
coresPerFork 2
memoryInGbPerFork 12
numberOfShards generalPurpose.numberOfShards
streamOutput generalPurpose.streamOutput
coresPerFork generalPurpose.coresPerFork
memoryInGbPerFork generalPurpose.memoryInGbPerFork
nodeTaints generalPurpose.nodeTaints
distribute DistributeTestsBy.METHOD
nodeTaints "big"
}
task allParallelUnitTest(type: ParallelTestGroup) {
dependsOn dependxiesModule
podLogLevel PodLogLevel.INFO
testGroups "test"
numberOfShards 10
streamOutput false
coresPerFork 2
memoryInGbPerFork 12
numberOfShards generalPurpose.numberOfShards
streamOutput generalPurpose.streamOutput
coresPerFork generalPurpose.coresPerFork
memoryInGbPerFork generalPurpose.memoryInGbPerFork
nodeTaints generalPurpose.nodeTaints
distribute DistributeTestsBy.CLASS
nodeTaints "small"
}
task allParallelUnitAndIntegrationTest(type: ParallelTestGroup) {
dependsOn dependxiesModule
testGroups "test", "integrationTest"
numberOfShards 15
streamOutput false
coresPerFork 6
memoryInGbPerFork 10
numberOfShards generalPurpose.numberOfShards
streamOutput generalPurpose.streamOutput
coresPerFork generalPurpose.coresPerFork
memoryInGbPerFork generalPurpose.memoryInGbPerFork
nodeTaints generalPurpose.nodeTaints
distribute DistributeTestsBy.METHOD
nodeTaints "big"
}
task parallelRegressionTest(type: ParallelTestGroup) {
testGroups "test", "integrationTest", "smokeTest"
dependsOn dependxiesModule
numberOfShards 15
streamOutput false
coresPerFork 2
memoryInGbPerFork 10
numberOfShards generalPurpose.numberOfShards
streamOutput generalPurpose.streamOutput
coresPerFork generalPurpose.coresPerFork
memoryInGbPerFork generalPurpose.memoryInGbPerFork
nodeTaints generalPurpose.nodeTaints
distribute DistributeTestsBy.METHOD
nodeTaints "big"
}
task allParallelSmokeTest(type: ParallelTestGroup) {
testGroups "smokeTest"
dependsOn dependxiesModule
numberOfShards 4
streamOutput false
coresPerFork 6
memoryInGbPerFork 10
distribute DistributeTestsBy.CLASS
nodeTaints "big"
numberOfShards generalPurpose.numberOfShards
streamOutput generalPurpose.streamOutput
coresPerFork generalPurpose.coresPerFork
memoryInGbPerFork generalPurpose.memoryInGbPerFork
nodeTaints generalPurpose.nodeTaints
distribute DistributeTestsBy.METHOD
}
task allParallelSlowIntegrationTest(type: ParallelTestGroup) {
testGroups "slowIntegrationTest"
dependsOn dependxiesModule
numberOfShards 4
streamOutput false
coresPerFork 6
memoryInGbPerFork 10
distribute DistributeTestsBy.CLASS
nodeTaints "big"
numberOfShards generalPurpose.numberOfShards
streamOutput generalPurpose.streamOutput
coresPerFork generalPurpose.coresPerFork
memoryInGbPerFork generalPurpose.memoryInGbPerFork
nodeTaints generalPurpose.nodeTaints
distribute DistributeTestsBy.METHOD
}
apply plugin: 'com.r3.testing.distributed-testing'
apply plugin: 'com.r3.testing.image-building'

View File

@ -21,7 +21,7 @@ object Configuration {
/**
* Describes a [Config] hiding sensitive data.
*/
fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue = { value -> ConfigValueFactory.fromAnyRef(value.toString()) }): ConfigValue?
fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue = { value -> ConfigValueFactory.fromAnyRef(value.toString()) }, options: Options): ConfigValue?
}
object Value {
@ -36,10 +36,11 @@ object Configuration {
*
* @throws ConfigException.Missing if the [Config] does not specify the value.
* @throws ConfigException.WrongType if the [Config] specifies a value of the wrong type.
* @throws ConfigException.BadValue if the [Config] specifies a value of the correct type, but this in unacceptable according to application-level validation rules..
* @throws ConfigException.BadValue if the [Config] specifies a value of the correct type, but this in unacceptable according to
* application-level validation rules.
*/
@Throws(ConfigException.Missing::class, ConfigException.WrongType::class, ConfigException.BadValue::class)
fun valueIn(configuration: Config): TYPE
fun valueIn(configuration: Config, options: Options): TYPE
/**
* Returns whether the value is specified by the [Config].
@ -50,27 +51,28 @@ object Configuration {
* Returns a value out of a [Config] if all is good, or null if no value is present. Otherwise, it throws an exception.
*
* @throws ConfigException.WrongType if the [Config] specifies a value of the wrong type.
* @throws ConfigException.BadValue if the [Config] specifies a value of the correct type, but this in unacceptable according to application-level validation rules..
* @throws ConfigException.BadValue if the [Config] specifies a value of the correct type, but this in unacceptable according to
* application-level validation rules.
*/
@Throws(ConfigException.WrongType::class, ConfigException.BadValue::class)
fun valueInOrNull(configuration: Config): TYPE? {
fun valueInOrNull(configuration: Config, options: Options): TYPE? {
return when {
isSpecifiedBy(configuration) -> valueIn(configuration)
isSpecifiedBy(configuration) -> valueIn(configuration, options)
else -> null
}
}
}
/**
* Able to parse a value from a [Config] and [Configuration.Validation.Options], returning a [Valid] result containing either the value itself, or some [Configuration.Validation.Error]s.
* Able to parse a value from a [Config] and [Configuration.Options], returning a [Valid] result containing either the value itself, or some [Configuration.Validation.Error]s.
*/
interface Parser<VALUE> {
/**
* Returns a [Valid] wrapper either around a valid value extracted from the [Config], or around a set of [Configuration.Validation.Error] with details about what went wrong.
*/
fun parse(configuration: Config, options: Configuration.Validation.Options = Configuration.Validation.Options.defaults): Valid<VALUE>
fun parse(configuration: Config, options: Options = Options.defaults): Valid<VALUE>
}
}
@ -109,11 +111,6 @@ object Configuration {
*/
interface Definition<TYPE> : Configuration.Property.Metadata, Configuration.Validator, Configuration.Value.Extractor<TYPE>, Configuration.Describer, Configuration.Value.Parser<TYPE> {
/**
* Validates target [Config] with default [Configuration.Validation.Options].
*/
fun validate(target: Config): Valid<Config> = validate(target, Configuration.Validation.Options.defaults)
override fun isSpecifiedBy(configuration: Config): Boolean = configuration.hasPath(key)
/**
@ -181,9 +178,8 @@ object Configuration {
fun <MAPPED> map(mappedTypeName: String, convert: (TYPE) -> MAPPED): Standard<MAPPED> = mapValid(mappedTypeName) { value -> valid(convert.invoke(value)) }
}
override fun parse(configuration: Config, options: Configuration.Validation.Options): Validated<TYPE, Validation.Error> {
return validate(configuration, options).mapValid { config -> valid(valueIn(config)) }
override fun parse(configuration: Config, options: Configuration.Options): Validated<TYPE, Validation.Error> {
return validate(configuration, options).mapValid { config -> valid(valueIn(config, options)) }
}
companion object {
@ -199,7 +195,6 @@ object Configuration {
* Returns a [Configuration.Property.Definition.Standard] with value of type [Int].
*/
fun int(key: String, sensitive: Boolean = false): Standard<Int> = long(key, sensitive).mapValid { value ->
try {
valid(Math.toIntExact(value))
} catch (e: ArithmeticException) {
@ -210,18 +205,17 @@ object Configuration {
/**
* Returns a [Configuration.Property.Definition.Standard] with value of type [Boolean].
*/
fun boolean(key: String, sensitive: Boolean = false): Standard<Boolean> = StandardProperty(key, Boolean::class.javaObjectType.simpleName, Config::getBoolean, Config::getBooleanList, sensitive)
fun boolean(key: String, sensitive: Boolean = false): Standard<Boolean> = StandardProperty(key, Boolean::class.javaObjectType.simpleName, { config, path, _ -> config.getBoolean(path) }, { config, path, _ -> config.getBooleanList(path) }, sensitive)
/**
* Returns a [Configuration.Property.Definition.Standard] with value of type [Double].
*/
fun double(key: String, sensitive: Boolean = false): Standard<Double> = StandardProperty(key, Double::class.javaObjectType.simpleName, Config::getDouble, Config::getDoubleList, sensitive)
fun double(key: String, sensitive: Boolean = false): Standard<Double> = StandardProperty(key, Double::class.javaObjectType.simpleName, { config, path, _ -> config.getDouble(path) }, { config, path, _ -> config.getDoubleList(path) }, sensitive)
/**
* Returns a [Configuration.Property.Definition.Standard] with value of type [Float].
*/
fun float(key: String, sensitive: Boolean = false): Standard<Float> = double(key, sensitive).mapValid { value ->
val floatValue = value.toFloat()
if (floatValue.isInfinite() || floatValue.isNaN()) {
invalid<Float, Configuration.Validation.Error>(Configuration.Validation.Error.BadValue.of(key, Float::class.javaObjectType.simpleName, "Provided value exceeds Float range."))
@ -233,24 +227,43 @@ object Configuration {
/**
* Returns a [Configuration.Property.Definition.Standard] with value of type [String].
*/
fun string(key: String, sensitive: Boolean = false): Standard<String> = StandardProperty(key, String::class.java.simpleName, Config::getString, Config::getStringList, sensitive)
fun string(key: String, sensitive: Boolean = false): Standard<String> = StandardProperty(
key,
String::class.java.simpleName,
{ config, path, _ -> config.getString(path) },
{ config, path, _ -> config.getStringList(path) },
sensitive
)
/**
* Returns a [Configuration.Property.Definition.Standard] with value of type [Duration].
*/
fun duration(key: String, sensitive: Boolean = false): Standard<Duration> = StandardProperty(key, Duration::class.java.simpleName, Config::getDuration, Config::getDurationList, sensitive)
fun duration(key: String, sensitive: Boolean = false): Standard<Duration> = StandardProperty(key, Duration::class.java.simpleName, { config, path, _ -> config.getDuration(path) }, { config, path, _ -> config.getDurationList(path) }, sensitive)
/**
* Returns a [Configuration.Property.Definition.Standard] with value of type [ConfigObject].
* It supports an optional [Configuration.Schema], which is used for validation and more when provided.
*/
fun nestedObject(key: String, schema: Schema? = null, sensitive: Boolean = false): Standard<ConfigObject> = StandardProperty(key, ConfigObject::class.java.simpleName, Config::getObject, Config::getObjectList, sensitive, schema)
fun nestedObject(key: String, schema: Schema? = null, sensitive: Boolean = false): Standard<ConfigObject> = StandardProperty(
key,
ConfigObject::class.java.simpleName,
{ config, path, _ -> config.getObject(path) },
{ config, path, _ -> config.getObjectList(path) },
sensitive,
schema
)
/**
* Returns a [Configuration.Property.Definition.Standard] with value of type [ENUM].
* This property expects a value in the configuration matching one of the cases of [ENUM], as text, in uppercase.
*/
fun <ENUM : Enum<ENUM>> enum(key: String, enumClass: KClass<ENUM>, sensitive: Boolean = false): Standard<ENUM> = StandardProperty(key, enumClass.java.simpleName, { conf: Config, propertyKey: String -> conf.getEnum(enumClass.java, propertyKey) }, { conf: Config, propertyKey: String -> conf.getEnumList(enumClass.java, propertyKey) }, sensitive)
fun <ENUM : Enum<ENUM>> enum(key: String, enumClass: KClass<ENUM>, sensitive: Boolean = false): Standard<ENUM> = StandardProperty(
key,
enumClass.java.simpleName,
{ conf: Config, propertyKey: String, _ -> conf.getEnum(enumClass.java, propertyKey) },
{ conf: Config, propertyKey: String, _ -> conf.getEnumList(enumClass.java, propertyKey) },
sensitive
)
}
}
}
@ -275,12 +288,7 @@ object Configuration {
*/
val properties: Set<Property.Definition<*>>
/**
* Validates target [Config] with default [Configuration.Validation.Options].
*/
fun validate(target: Config): Valid<Config> = validate(target, Configuration.Validation.Options.defaults)
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options): ConfigValue
companion object {
@ -368,35 +376,35 @@ object Configuration {
override fun description() = schema.description()
override fun validate(target: Config, options: Validation.Options) = schema.validate(target, options)
override fun validate(target: Config, options: Options) = schema.validate(target, options)
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue) = schema.describe(configuration, serialiseValue)
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options) = schema.describe(configuration, serialiseValue, options)
final override fun parse(configuration: Config, options: Configuration.Validation.Options): Valid<VALUE> = validate(configuration, options).mapValid(::parseValid)
final override fun parse(configuration: Config, options: Options): Valid<VALUE> = validate(configuration, options).mapValid { parseValid(it, options) }
/**
* Implement to define further mapping and validation logic, assuming the underlying raw [Config] is correct in terms of this [Configuration.Specification].
*/
protected abstract fun parseValid(configuration: Config): Valid<VALUE>
protected abstract fun parseValid(configuration: Config, options: Options): Valid<VALUE>
}
/**
* Validation and processing options.
* @property strict whether to raise unknown property keys as errors.
*/
class Options(val strict: Boolean = false) {
companion object {
/**
* Default [Config] options, without [strict] parsing enabled.
*/
val defaults: Configuration.Options = Options()
}
}
object Validation {
/**
* [Config] validation options.
* @property strict whether to raise unknown property keys as errors.
*/
data class Options(val strict: Boolean) {
companion object {
/**
* Default [Config] validation options, without [strict] parsing enabled.
*/
val defaults: Configuration.Validation.Options = Options(strict = false)
}
}
/**
* Super-type for the errors raised by the parsing and validation of a [Config] object.
*
@ -531,7 +539,7 @@ object Configuration {
}
/**
* Raised when a key-value pair appeared in the [Config] object without a matching property in the [Configuration.Schema], and [Configuration.Validation.Options.strict] was enabled.
* Raised when a key-value pair appeared in the [Config] object without a matching property in the [Configuration.Schema], and [Configuration.Options.strict] was enabled.
*/
class Unknown private constructor(override val keyName: String, containingPath: List<String> = emptyList()) : Configuration.Validation.Error(keyName, null, message(keyName), containingPath) {
@ -586,5 +594,5 @@ object Configuration {
/**
* Defines the ability to validate a [Config] object, producing a valid [Config] or a set of [Configuration.Validation.Error].
*/
interface Validator : net.corda.common.validation.internal.Validator<Config, Configuration.Validation.Error, Configuration.Validation.Options>
interface Validator : net.corda.common.validation.internal.Validator<Config, Configuration.Validation.Error, Configuration.Options>
}

View File

@ -5,10 +5,9 @@ import net.corda.common.validation.internal.Validated
import net.corda.common.validation.internal.Validated.Companion.invalid
import net.corda.common.validation.internal.Validated.Companion.valid
internal class LongProperty(key: String, sensitive: Boolean = false) : StandardProperty<Long>(key, Long::class.javaObjectType.simpleName, Config::getLong, Config::getLongList, sensitive) {
override fun validate(target: Config, options: Configuration.Validation.Options): Valid<Config> {
internal class LongProperty(key: String, sensitive: Boolean = false) : StandardProperty<Long>(key, Long::class.javaObjectType.simpleName, { config, path, _ -> config.getLong(path) }, { config, path, _ -> config.getLongList(path) }, sensitive) {
override fun validate(target: Config, options: Configuration.Options): Valid<Config> {
val validated = super.validate(target, options)
if (validated.isValid && target.getValue(key).unwrapped().toString().contains(".")) {
return invalid(ConfigException.WrongType(target.origin(), key, Long::class.javaObjectType.simpleName, Double::class.javaObjectType.simpleName).toValidationError(key, typeName))
@ -17,9 +16,11 @@ internal class LongProperty(key: String, sensitive: Boolean = false) : StandardP
}
}
internal open class StandardProperty<TYPE : Any>(override val key: String, typeNameArg: String, private val extractSingleValue: (Config, String) -> TYPE, internal val extractListValue: (Config, String) -> List<TYPE>, override val isSensitive: Boolean = false, final override val schema: Configuration.Schema? = null) : Configuration.Property.Definition.Standard<TYPE> {
typealias ValueSelector<T> = (Config, String, Configuration.Options) -> T
override fun valueIn(configuration: Config) = extractSingleValue.invoke(configuration, key)
internal open class StandardProperty<TYPE : Any>(override val key: String, typeNameArg: String, private val extractSingleValue: ValueSelector<TYPE>, internal val extractListValue: ValueSelector<List<TYPE>>, override val isSensitive: Boolean = false, final override val schema: Configuration.Schema? = null) : Configuration.Property.Definition.Standard<TYPE> {
override fun valueIn(configuration: Config, options: Configuration.Options) = extractSingleValue.invoke(configuration, key, options)
override val typeName: String = schema?.let { "#${it.name ?: "Object@$key"}" } ?: typeNameArg
@ -29,20 +30,18 @@ internal open class StandardProperty<TYPE : Any>(override val key: String, typeN
override fun list(): Configuration.Property.Definition.RequiredList<TYPE> = ListProperty(this)
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue {
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options): ConfigValue {
if (isSensitive) {
return valueDescription(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER, serialiseValue)
}
return schema?.describe(configuration.getConfig(key), serialiseValue) ?: valueDescription(valueIn(configuration), serialiseValue)
return schema?.describe(configuration.getConfig(key), serialiseValue, options) ?: valueDescription(valueIn(configuration, options), serialiseValue)
}
override val isMandatory = true
override fun validate(target: Config, options: Configuration.Validation.Options): Valid<Config> {
override fun validate(target: Config, options: Configuration.Options): Valid<Config> {
val errors = mutableSetOf<Configuration.Validation.Error>()
errors += errorsWhenExtractingValue(target)
errors += errorsWhenExtractingValue(target, options)
if (errors.isEmpty()) {
schema?.let { nestedSchema ->
val nestedConfig: Config? = target.getConfig(key)
@ -61,15 +60,19 @@ private class ListProperty<TYPE : Any>(delegate: StandardProperty<TYPE>) : Requi
override val typeName: String = "List<${delegate.typeName}>"
override fun valueIn(configuration: Config): List<TYPE> = delegate.extractListValue.invoke(configuration, key)
override fun validate(target: Config, options: Configuration.Validation.Options): Valid<Config> {
override fun valueIn(configuration: Config, options: Configuration.Options): List<TYPE> = delegate.extractListValue.invoke(configuration, key, options)
override fun validate(target: Config, options: Configuration.Options): Valid<Config> {
val errors = mutableSetOf<Configuration.Validation.Error>()
errors += errorsWhenExtractingValue(target)
errors += errorsWhenExtractingValue(target, options)
if (errors.isEmpty()) {
delegate.schema?.let { schema ->
errors += valueIn(target).asSequence().map { element -> element as ConfigObject }.map(ConfigObject::toConfig).mapIndexed { index, targetConfig -> schema.validate(targetConfig, options).errors.map { error -> error.withContainingPath(*error.containingPath(index).toTypedArray()) } }.fold(emptyList<Configuration.Validation.Error>()) { one, other -> one + other }.toSet()
errors += valueIn(target, options).asSequence()
.map { element -> element as ConfigObject }
.map(ConfigObject::toConfig)
.mapIndexed { index, targetConfig -> schema.validate(targetConfig, options).errors.map { error -> error.withContainingPath(*error.containingPath(index).toTypedArray()) } }
.fold(emptyList<Configuration.Validation.Error>()) { one, other -> one + other }
.toSet()
}
}
return Validated.withResult(target, errors)
@ -77,17 +80,16 @@ private class ListProperty<TYPE : Any>(delegate: StandardProperty<TYPE>) : Requi
override fun <MAPPED> mapValid(mappedTypeName: String, convert: (List<TYPE>) -> Validated<MAPPED, Configuration.Validation.Error>): Configuration.Property.Definition.Required<MAPPED> = ListMappingProperty(this, mappedTypeName, convert)
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue {
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options): ConfigValue {
if (isSensitive) {
return valueDescription(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER, serialiseValue)
}
return when {
delegate.schema != null -> {
val elementsDescription = valueIn(configuration).asSequence().map { it as ConfigObject }.map(ConfigObject::toConfig).map { delegate.schema.describe(it, serialiseValue) }.toList()
val elementsDescription = valueIn(configuration, options).asSequence().map { it as ConfigObject }.map(ConfigObject::toConfig).map { delegate.schema.describe(it, serialiseValue, options) }.toList()
ConfigValueFactory.fromIterable(elementsDescription)
}
else -> valueDescription(valueIn(configuration), serialiseValue)
else -> valueDescription(valueIn(configuration, options), serialiseValue)
}
}
@ -106,16 +108,17 @@ private class OptionalPropertyWithDefault<TYPE>(delegate: Configuration.Property
override val typeName: String = delegate.typeName.removeSuffix("?")
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue? = delegate.describe(configuration, serialiseValue) ?: valueDescription(if (isSensitive) Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER else defaultValue, serialiseValue)
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options): ConfigValue? = delegate.describe(configuration, serialiseValue, options) ?: valueDescription(if (isSensitive) Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER else defaultValue, serialiseValue)
override fun valueIn(configuration: Config): TYPE = delegate.valueIn(configuration) ?: defaultValue
override fun valueIn(configuration: Config, options: Configuration.Options): TYPE = delegate.valueIn(configuration, options) ?: defaultValue
override fun validate(target: Config, options: Configuration.Validation.Options): Valid<Config> = delegate.validate(target, options)
override fun validate(target: Config, options: Configuration.Options): Valid<Config> = delegate.validate(target, options)
}
private class FunctionalProperty<TYPE, MAPPED>(delegate: Configuration.Property.Definition.Standard<TYPE>, private val mappedTypeName: String, internal val extractListValue: (Config, String) -> List<TYPE>, private val convert: (TYPE) -> Valid<MAPPED>) : RequiredDelegatedProperty<MAPPED, Configuration.Property.Definition.Standard<TYPE>>(delegate), Configuration.Property.Definition.Standard<MAPPED> {
private class FunctionalProperty<TYPE, MAPPED>(delegate: Configuration.Property.Definition.Standard<TYPE>, private val mappedTypeName: String, internal val extractListValue: ValueSelector<List<TYPE>>, private val convert: (TYPE) -> Valid<MAPPED>)
: RequiredDelegatedProperty<MAPPED, Configuration.Property.Definition.Standard<TYPE>>(delegate), Configuration.Property.Definition.Standard<MAPPED> {
override fun valueIn(configuration: Config) = convert.invoke(delegate.valueIn(configuration)).value()
override fun valueIn(configuration: Config, options: Configuration.Options) = convert.invoke(delegate.valueIn(configuration, options)).value()
override val typeName: String = if (super.typeName == "#$mappedTypeName") super.typeName else "$mappedTypeName(${super.typeName})"
@ -123,29 +126,31 @@ private class FunctionalProperty<TYPE, MAPPED>(delegate: Configuration.Property.
override fun list(): Configuration.Property.Definition.RequiredList<MAPPED> = FunctionalListProperty(this)
override fun validate(target: Config, options: Configuration.Validation.Options): Valid<Config> {
override fun validate(target: Config, options: Configuration.Options): Valid<Config> {
val errors = mutableSetOf<Configuration.Validation.Error>()
errors += delegate.validate(target, options).errors
if (errors.isEmpty()) {
errors += convert.invoke(delegate.valueIn(target)).mapErrors { error -> error.with(delegate.key, mappedTypeName) }.errors
errors += convert.invoke(delegate.valueIn(target, options)).mapErrors { error -> error.with(delegate.key, mappedTypeName) }.errors
}
return Validated.withResult(target, errors)
}
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue) = delegate.describe(configuration, serialiseValue)
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options) = delegate.describe(configuration, serialiseValue, options)
}
private class FunctionalListProperty<RAW, TYPE>(delegate: FunctionalProperty<RAW, TYPE>) : RequiredDelegatedProperty<List<TYPE>, FunctionalProperty<RAW, TYPE>>(delegate), Configuration.Property.Definition.RequiredList<TYPE> {
override val typeName: String = "List<${super.typeName}>"
override fun valueIn(configuration: Config): List<TYPE> = delegate.extractListValue.invoke(configuration, key).asSequence().map { configObject(key to ConfigValueFactory.fromAnyRef(it)) }.map(ConfigObject::toConfig).map(delegate::valueIn).toList()
override fun validate(target: Config, options: Configuration.Validation.Options): Valid<Config> {
override fun valueIn(configuration: Config, options: Configuration.Options): List<TYPE> = delegate.extractListValue.invoke(configuration, key, options).asSequence()
.map { configObject(key to ConfigValueFactory.fromAnyRef(it)) }
.map(ConfigObject::toConfig)
.map { delegate.valueIn(it, options) }
.toList()
override fun validate(target: Config, options: Configuration.Options): Valid<Config> {
val list = try {
delegate.extractListValue.invoke(target, key)
delegate.extractListValue.invoke(target, key, options)
} catch (e: ConfigException) {
if (isErrorExpected(e)) {
return invalid(e.toValidationError(key, typeName))
@ -153,7 +158,11 @@ private class FunctionalListProperty<RAW, TYPE>(delegate: FunctionalProperty<RAW
throw e
}
}
val errors = list.asSequence().map { configObject(key to ConfigValueFactory.fromAnyRef(it)) }.mapIndexed { index, value -> delegate.validate(value.toConfig(), options).errors.map { error -> error.withContainingPath(*error.containingPath(index).toTypedArray()) } }.fold(emptyList<Configuration.Validation.Error>()) { one, other -> one + other }.toSet()
val errors = list.asSequence()
.map { configObject(key to ConfigValueFactory.fromAnyRef(it)) }
.mapIndexed { index, value -> delegate.validate(value.toConfig(), options).errors.map { error -> error.withContainingPath(*error.containingPath(index).toTypedArray()) } }
.fold(emptyList<Configuration.Validation.Error>()) { one, other -> one + other }
.toSet()
return Validated.withResult(target, errors)
}
@ -165,12 +174,11 @@ private class FunctionalListProperty<RAW, TYPE>(delegate: FunctionalProperty<RAW
}
}
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue {
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options): ConfigValue {
if (isSensitive) {
return valueDescription(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER, serialiseValue)
}
return delegate.schema?.let { schema -> valueDescription(valueIn(configuration).asSequence().map { element -> valueDescription(element, serialiseValue) }.map { it as ConfigObject }.map(ConfigObject::toConfig).map { schema.describe(it, serialiseValue) }.toList(), serialiseValue) } ?: valueDescription(valueIn(configuration), serialiseValue)
return delegate.schema?.let { schema -> valueDescription(valueIn(configuration, options).asSequence() .map { element -> valueDescription(element, serialiseValue) } .map { it as ConfigObject } .map(ConfigObject::toConfig) .map { schema.describe(it, serialiseValue, options) } .toList(), serialiseValue) } ?: valueDescription(valueIn(configuration, options), serialiseValue)
}
override fun <MAPPED> mapValid(mappedTypeName: String, convert: (List<TYPE>) -> Validated<MAPPED, Configuration.Validation.Error>): Configuration.Property.Definition.Required<MAPPED> = ListMappingProperty(this, mappedTypeName, convert)
@ -187,18 +195,16 @@ private class OptionalDelegatedProperty<TYPE>(private val delegate: Configuratio
override val typeName: String = "${delegate.typeName}?"
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue) = if (isSpecifiedBy(configuration)) delegate.describe(configuration, serialiseValue) else null
override fun valueIn(configuration: Config): TYPE? {
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options) = if (isSpecifiedBy(configuration)) delegate.describe(configuration, serialiseValue, options) else null
override fun valueIn(configuration: Config, options: Configuration.Options): TYPE? {
return when {
isSpecifiedBy(configuration) -> delegate.valueIn(configuration)
isSpecifiedBy(configuration) -> delegate.valueIn(configuration, options)
else -> null
}
}
override fun validate(target: Config, options: Configuration.Validation.Options): Valid<Config> {
override fun validate(target: Config, options: Configuration.Options): Valid<Config> {
val result = delegate.validate(target, options)
val errors = result.errors
val missingValueError = errors.asSequence().filterIsInstance<Configuration.Validation.Error.MissingValue>().filter { it.pathAsString == key }.singleOrNull()
@ -221,18 +227,17 @@ private abstract class RequiredDelegatedProperty<TYPE, DELEGATE : Configuration.
private class ListMappingProperty<TYPE, MAPPED>(private val delegate: Configuration.Property.Definition.RequiredList<TYPE>, private val mappedTypeName: String, private val convert: (List<TYPE>) -> Validated<MAPPED, Configuration.Validation.Error>) : Configuration.Property.Definition.Required<MAPPED> {
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue? = delegate.describe(configuration, serialiseValue)
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options): ConfigValue? = delegate.describe(configuration, serialiseValue, options)
override fun valueIn(configuration: Config) = convert.invoke(delegate.valueIn(configuration)).value()
override fun valueIn(configuration: Config, options: Configuration.Options) = convert.invoke(delegate.valueIn(configuration, options)).value()
override fun optional(): Configuration.Property.Definition.Optional<MAPPED> = OptionalDelegatedProperty(this)
override fun validate(target: Config, options: Configuration.Validation.Options): Validated<Config, Configuration.Validation.Error> {
override fun validate(target: Config, options: Configuration.Options): Validated<Config, Configuration.Validation.Error> {
val errors = mutableSetOf<Configuration.Validation.Error>()
errors += delegate.validate(target, options).errors
if (errors.isEmpty()) {
errors += convert.invoke(delegate.valueIn(target)).mapErrors { error -> error.with(delegate.key, mappedTypeName) }.errors
errors += convert.invoke(delegate.valueIn(target, options)).mapErrors { error -> error.with(delegate.key, mappedTypeName) }.errors
}
return Validated.withResult(target, errors)
}
@ -248,7 +253,6 @@ private class ListMappingProperty<TYPE, MAPPED>(private val delegate: Configurat
}
fun ConfigException.toValidationError(keyName: String? = null, typeName: String): Configuration.Validation.Error {
val toError = when (this) {
is ConfigException.Missing -> Configuration.Validation.Error.MissingValue.Companion::of
is ConfigException.WrongType -> Configuration.Validation.Error.WrongType.Companion::of
@ -260,10 +264,9 @@ fun ConfigException.toValidationError(keyName: String? = null, typeName: String)
return toError.invoke(message!!, keyName, typeName, emptyList())
}
private fun Configuration.Property.Definition<*>.errorsWhenExtractingValue(target: Config): Set<Configuration.Validation.Error> {
private fun Configuration.Property.Definition<*>.errorsWhenExtractingValue(target: Config, options: Configuration.Options): Set<Configuration.Validation.Error> {
try {
valueIn(target)
valueIn(target, options)
return emptySet()
} catch (exception: ConfigException) {
if (isErrorExpected(exception)) {

View File

@ -16,7 +16,7 @@ internal class Schema(override val name: String?, unorderedProperties: Iterable<
}
}
override fun validate(target: Config, options: Configuration.Validation.Options): Valid<Config> {
override fun validate(target: Config, options: Configuration.Options): Valid<Config> {
val propertyErrors = properties.flatMap { property ->
property.validate(target, options).errors
@ -47,9 +47,9 @@ internal class Schema(override val name: String?, unorderedProperties: Iterable<
return description.toString()
}
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue {
override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue, options: Configuration.Options): ConfigValue {
return properties.asSequence().map { it.key to it.describe(configuration, serialiseValue) }.filter { it.second != null }.fold(configObject()) { config, (key, value) -> config.withValue(key, value) }
return properties.asSequence().map { it.key to it.describe(configuration, serialiseValue, options) }.filter { it.second != null }.fold(configObject()) { config, (key, value) -> config.withValue(key, value) }
}
override fun equals(other: Any?): Boolean {

View File

@ -21,7 +21,11 @@ inline fun <TYPE, reified MAPPED> Configuration.Property.Definition.RequiredList
inline fun <TYPE, reified MAPPED> Configuration.Property.Definition.RequiredList<TYPE>.map(noinline convert: (List<TYPE>) -> MAPPED): Configuration.Property.Definition.Required<MAPPED> = map(MAPPED::class.java.simpleName, convert)
operator fun <TYPE> Config.get(property: Configuration.Property.Definition<TYPE>): TYPE = property.valueIn(this)
fun Config.withOptions(options: Configuration.Options) = ConfigurationWithOptions(this, options)
data class ConfigurationWithOptions(private val config: Config, private val options: Configuration.Options) {
operator fun <TYPE> get(property: Configuration.Value.Extractor<TYPE>): TYPE = property.valueIn(config, options)
}
inline fun <reified NESTED : Any> Configuration.Specification<*>.nested(specification: Configuration.Specification<NESTED>, key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard<NESTED> = nestedObject(schema = specification, key = key, sensitive = sensitive).map(ConfigObject::toConfig).mapValid { value -> specification.parse(value) }
@ -66,15 +70,6 @@ internal typealias Valid<TARGET> = Validated<TARGET, Configuration.Validation.Er
internal fun <TYPE> valid(target: TYPE) = Validated.valid<TYPE, Configuration.Validation.Error>(target)
/**
* Value extracted from a configuration file is a function of the actual value specified and configuration options.
* E.g. password value may be stored in the encrypted form rather than in a clear text.
*/
data class ConfigurationWithOptions(private val config: Config, private val options: Configuration.Validation.Options) {
operator fun <TYPE> get(property: Configuration.Property.Definition<TYPE>): TYPE = property.valueIn(config)
operator fun <TYPE> get(property: Configuration.Value.Extractor<TYPE>): TYPE = property.valueIn(config)
}
/**
* Helper interface to mark objects that will have [ConfigurationWithOptions] in them.
*/

View File

@ -6,21 +6,17 @@ import net.corda.common.configuration.parsing.internal.Valid
import net.corda.common.configuration.parsing.internal.valid
internal class VersionExtractor(versionPath: String, versionDefaultValue: Int) : Configuration.Version.Extractor {
private val containingPath = versionPath.split(".").let { if (it.size > 1) it.subList(0, it.size - 1) else null }
private val key = versionPath.split(".").last()
private val spec = Spec(key, versionDefaultValue, containingPath?.joinToString("."))
override fun parse(configuration: Config, options: Configuration.Validation.Options): Valid<Int> {
override fun parse(configuration: Config, options: Configuration.Options): Valid<Int> {
return spec.parse(configuration)
}
private class Spec(key: String, versionDefaultValue: Int, prefix: String?) : Configuration.Specification<Int>("Version", prefix) {
private val version by int(key = key).optional().withDefaultValue(versionDefaultValue)
override fun parseValid(configuration: Config) = valid(version.valueIn(configuration))
override fun parseValid(configuration: Config, options: Configuration.Options) = valid(version.valueIn(configuration, options))
}
}

View File

@ -21,8 +21,8 @@ class PropertyTest {
assertThat(property.key).isEqualTo(key)
assertThat(property.isMandatory).isTrue()
assertThat(property.isSpecifiedBy(configuration)).isTrue()
assertThat(property.valueIn(configuration)).isEqualTo(value)
assertThat(configuration[property]).isEqualTo(value)
assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(value)
assertThat(configuration.withOptions(Configuration.Options.defaults)[property]).isEqualTo(value)
}
@Test(timeout=300_000)
@ -38,7 +38,7 @@ class PropertyTest {
assertThat(property.key).isEqualTo(key)
assertThat(property.isMandatory).isTrue()
assertThat(property.isSpecifiedBy(configuration)).isTrue()
assertThatThrownBy { property.valueIn(configuration) }.isInstanceOf(ConfigException.WrongType::class.java)
assertThatThrownBy { property.valueIn(configuration, Configuration.Options.defaults) }.isInstanceOf(ConfigException.WrongType::class.java)
}
@Test(timeout=300_000)
@ -54,7 +54,7 @@ class PropertyTest {
assertThat(property.key).isEqualTo(key)
assertThat(property.isMandatory).isTrue()
assertThat(property.isSpecifiedBy(configuration)).isTrue()
assertThat(property.valueIn(configuration)).isEqualTo(value)
assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(value)
}
@Test(timeout=300_000)
@ -70,7 +70,7 @@ class PropertyTest {
assertThat(property.key).isEqualTo(key)
assertThat(property.isMandatory).isTrue()
assertThat(property.isSpecifiedBy(configuration)).isTrue()
assertThat(property.valueIn(configuration)).isEqualTo(value.max())
assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(value.max())
}
@Test(timeout=300_000)
@ -85,7 +85,7 @@ class PropertyTest {
assertThat(property.key).isEqualTo(key)
assertThat(property.isMandatory).isFalse()
assertThat(property.isSpecifiedBy(configuration)).isFalse()
assertThat(property.valueIn(configuration)).isEqualTo(null)
assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(null)
}
@Test(timeout=300_000)
@ -101,7 +101,7 @@ class PropertyTest {
assertThat(property.key).isEqualTo(key)
assertThat(property.isMandatory).isTrue()
assertThat(property.isSpecifiedBy(configuration)).isTrue()
assertThat(property.valueIn(configuration)).isEqualTo(value.max())
assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(value.max())
}
@Test(timeout=300_000)
@ -116,7 +116,7 @@ class PropertyTest {
assertThat(property.key).isEqualTo(key)
assertThat(property.isMandatory).isFalse()
assertThat(property.isSpecifiedBy(configuration)).isFalse()
assertThat(property.valueIn(configuration)).isEqualTo(null)
assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(null)
}
@Test(timeout=300_000)
@ -132,7 +132,7 @@ class PropertyTest {
assertThat(property.key).isEqualTo(key)
assertThat(property.isMandatory).isFalse()
assertThat(property.isSpecifiedBy(configuration)).isTrue()
assertThat(property.valueIn(configuration)).isEqualTo(value)
assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(value)
}
@Test(timeout=300_000)
@ -147,7 +147,7 @@ class PropertyTest {
assertThat(property.key).isEqualTo(key)
assertThat(property.isMandatory).isFalse()
assertThat(property.isSpecifiedBy(configuration)).isFalse()
assertThat(property.valueIn(configuration)).isNull()
assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isNull()
}
@ -164,7 +164,7 @@ class PropertyTest {
assertThat(property.key).isEqualTo(key)
assertThat(property.isMandatory).isFalse()
assertThat(property.isSpecifiedBy(configuration)).isFalse()
assertThat(property.valueIn(configuration)).isEqualTo(defaultValue)
assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(defaultValue)
}
@Test(timeout=300_000)
@ -179,7 +179,7 @@ class PropertyTest {
assertThat(property.key).isEqualTo(key)
assertThat(property.isMandatory).isTrue()
assertThat(property.isSpecifiedBy(configuration)).isFalse()
assertThatThrownBy { property.valueIn(configuration) }.isInstanceOf(ConfigException.Missing::class.java)
assertThatThrownBy { property.valueIn(configuration, Configuration.Options.defaults) }.isInstanceOf(ConfigException.Missing::class.java)
}
@Test(timeout=300_000)
@ -195,7 +195,7 @@ class PropertyTest {
assertThat(property.key).isEqualTo(key)
assertThat(property.isMandatory).isFalse()
assertThat(property.isSpecifiedBy(configuration)).isTrue()
assertThat(property.valueIn(configuration)).isEqualTo(value)
assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(value)
}
@Test(timeout=300_000)
@ -211,7 +211,7 @@ class PropertyTest {
assertThat(property.key).isEqualTo(key)
assertThat(property.isMandatory).isFalse()
assertThat(property.isSpecifiedBy(configuration)).isTrue()
assertThatThrownBy { property.valueIn(configuration) }.isInstanceOf(ConfigException.WrongType::class.java)
assertThatThrownBy { property.valueIn(configuration, Configuration.Options.defaults) }.isInstanceOf(ConfigException.WrongType::class.java)
}
@Test(timeout=300_000)
@ -226,7 +226,7 @@ class PropertyTest {
assertThat(property.key).isEqualTo(key)
assertThat(property.isMandatory).isFalse()
assertThat(property.isSpecifiedBy(configuration)).isFalse()
assertThat(property.valueIn(configuration)).isNull()
assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isNull()
}
@Test(timeout=300_000)
@ -242,6 +242,6 @@ class PropertyTest {
assertThat(property.key).isEqualTo(key)
assertThat(property.isMandatory).isFalse()
assertThat(property.isSpecifiedBy(configuration)).isFalse()
assertThat(property.valueIn(configuration)).isEqualTo(defaultValue)
assertThat(property.valueIn(configuration, Configuration.Options.defaults)).isEqualTo(defaultValue)
}
}

View File

@ -15,7 +15,7 @@ class PropertyValidationTest {
val property = Configuration.Property.Definition.long(key)
assertThat(property.validate(configuration).errors).satisfies { errors ->
assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors ->
assertThat(errors).hasSize(1)
assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error ->
@ -34,7 +34,7 @@ class PropertyValidationTest {
val property = Configuration.Property.Definition.long(key)
assertThat(property.validate(configuration).errors).satisfies { errors ->
assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors ->
assertThat(errors).hasSize(1)
assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error ->
@ -53,7 +53,7 @@ class PropertyValidationTest {
val property = Configuration.Property.Definition.long(key).list()
assertThat(property.validate(configuration).errors).satisfies { errors ->
assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors ->
assertThat(errors).hasSize(1)
assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error ->
@ -72,7 +72,7 @@ class PropertyValidationTest {
val property = Configuration.Property.Definition.long(key).list()
assertThat(property.validate(configuration).errors).satisfies { errors ->
assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors ->
assertThat(errors).hasSize(1)
assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error ->
@ -94,7 +94,7 @@ class PropertyValidationTest {
val property = Configuration.Property.Definition.long(key).list().mapValid(::parseMax)
assertThat(property.validate(configuration).errors).isEmpty()
assertThat(property.validate(configuration, Configuration.Options.defaults).errors).isEmpty()
}
@Test(timeout=300_000)
@ -114,7 +114,7 @@ class PropertyValidationTest {
val property = Configuration.Property.Definition.long(key).list().mapValid(::parseMax)
assertThat(property.validate(configuration).errors).satisfies { errors ->
assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors ->
assertThat(errors).hasSize(1)
assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.BadValue::class.java) { error ->
@ -134,7 +134,7 @@ class PropertyValidationTest {
val configuration = configObject(key to false).toConfig()
assertThat(property.validate(configuration).errors).satisfies { errors ->
assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors ->
assertThat(errors).hasSize(1)
assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error ->
@ -154,7 +154,7 @@ class PropertyValidationTest {
val configuration = configObject(key to 1.2).toConfig()
assertThat(property.validate(configuration).errors).satisfies { errors ->
assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors ->
assertThat(errors).hasSize(1)
assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error ->
@ -174,7 +174,7 @@ class PropertyValidationTest {
val configuration = configObject(key to 1).toConfig()
assertThat(property.validate(configuration).isValid).isTrue()
assertThat(property.validate(configuration, Configuration.Options.defaults).isValid).isTrue()
}
@Test(timeout=300_000)
@ -186,7 +186,7 @@ class PropertyValidationTest {
val configuration = configObject(key to listOf(false, true)).toConfig()
assertThat(property.validate(configuration).errors).satisfies { errors ->
assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors ->
assertThat(errors).hasSize(1)
assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error ->
@ -206,7 +206,7 @@ class PropertyValidationTest {
val configuration = configObject(key to listOf(1, 2, 3)).toConfig()
assertThat(property.validate(configuration).errors).satisfies { errors ->
assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors ->
assertThat(errors).hasSize(1)
assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error ->
@ -226,7 +226,7 @@ class PropertyValidationTest {
val configuration = configObject(key to 1).toConfig()
assertThat(property.validate(configuration).errors).satisfies { errors ->
assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors ->
assertThat(errors).hasSize(1)
assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error ->
@ -249,7 +249,7 @@ class PropertyValidationTest {
val configuration = configObject(key to configObject(nestedKey to false)).toConfig()
assertThat(property.validate(configuration).errors).satisfies { errors ->
assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors ->
assertThat(errors).hasSize(1)
assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error ->
@ -272,7 +272,7 @@ class PropertyValidationTest {
val configuration = configObject(key to configObject()).toConfig()
assertThat(property.validate(configuration).errors).satisfies { errors ->
assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors ->
assertThat(errors).hasSize(1)
assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error ->
@ -295,7 +295,7 @@ class PropertyValidationTest {
val configuration = configObject(key to configObject(nestedKey to null)).toConfig()
assertThat(property.validate(configuration).errors).satisfies { errors ->
assertThat(property.validate(configuration, Configuration.Options.defaults).errors).satisfies { errors ->
assertThat(errors).hasSize(1)
assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error ->
@ -317,7 +317,7 @@ class PropertyValidationTest {
val configuration = configObject(key to configObject(nestedKey to false)).toConfig()
assertThat(property.validate(configuration).isValid).isTrue()
assertThat(property.validate(configuration, Configuration.Options.defaults).isValid).isTrue()
}
@Test(timeout=300_000)
@ -333,7 +333,7 @@ class PropertyValidationTest {
val configuration = configObject(key to value).toConfig()
assertThat(property.validate(configuration).isValid).isTrue()
assertThat(property.validate(configuration, Configuration.Options.defaults).isValid).isTrue()
}
@Test(timeout=300_000)
@ -350,7 +350,7 @@ class PropertyValidationTest {
val configuration = configObject(key to value).toConfig()
val result = property.validate(configuration)
val result = property.validate(configuration, Configuration.Options.defaults)
assertThat(result.errors).satisfies { errors ->

View File

@ -29,7 +29,7 @@ class SchemaTest {
val fooConfigSchema = Configuration.Schema.withProperties(name = "Foo") { setOf(boolean("prop4"), double("prop5")) }
val barConfigSchema = Configuration.Schema.withProperties(name = "Bar") { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) }
val result = barConfigSchema.validate(configuration)
val result = barConfigSchema.validate(configuration, Configuration.Options.defaults)
println(barConfigSchema.description())
assertThat(result.isValid).isTrue()
@ -59,17 +59,17 @@ class SchemaTest {
val fooConfigSchema = Configuration.Schema.withProperties { setOf(boolean("prop4"), double("prop5")) }
val barConfigSchema = Configuration.Schema.withProperties { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) }
val strictErrors = barConfigSchema.validate(configuration, Configuration.Validation.Options(strict = true)).errors
val strictErrors = barConfigSchema.validate(configuration, Configuration.Options(strict = true)).errors
assertThat(strictErrors).hasSize(2)
assertThat(strictErrors.filter { error -> error.keyName == "prop4" }).hasSize(1)
assertThat(strictErrors.filter { error -> error.keyName == "prop6" }).hasSize(1)
val errors = barConfigSchema.validate(configuration, Configuration.Validation.Options(strict = false)).errors
val errors = barConfigSchema.validate(configuration, Configuration.Options(strict = false)).errors
assertThat(errors).isEmpty()
val errorsWithDefaultOptions = barConfigSchema.validate(configuration).errors
val errorsWithDefaultOptions = barConfigSchema.validate(configuration, Configuration.Options.defaults).errors
assertThat(errorsWithDefaultOptions).isEmpty()
}
@ -98,7 +98,7 @@ class SchemaTest {
val fooConfigSchema = Configuration.Schema.withProperties { setOf(boolean("prop4"), double("prop5")) }
val barConfigSchema = Configuration.Schema.withProperties { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) }
val result = barConfigSchema.validate(configuration)
val result = barConfigSchema.validate(configuration, Configuration.Options.defaults)
assertThat(result.isValid).isTrue()
}
@ -127,7 +127,7 @@ class SchemaTest {
val fooConfigSchema = Configuration.Schema.withProperties { setOf(boolean("prop4"), double("prop5")) }
val barConfigSchema = Configuration.Schema.withProperties { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) }
val errors = barConfigSchema.validate(configuration).errors
val errors = barConfigSchema.validate(configuration, Configuration.Options.defaults).errors
errors.forEach(::println)
assertThat(errors).hasSize(2)
@ -154,7 +154,7 @@ class SchemaTest {
val fooConfigSchema = Configuration.Schema.withProperties(name = "Foo") { setOf(boolean("prop4"), string("prop5", sensitive = true)) }
val barConfigSchema = Configuration.Schema.withProperties(name = "Bar") { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) }
val printedConfiguration = barConfigSchema.describe(configuration)
val printedConfiguration = barConfigSchema.describe(configuration, options = Configuration.Options.defaults)
val description = printedConfiguration.serialize().also { println(it) }
@ -185,7 +185,7 @@ class SchemaTest {
val fooConfigSchema = Configuration.Schema.withProperties(name = "Foo") { setOf(boolean("prop4"), string("prop5", sensitive = true)) }
val barConfigSchema = Configuration.Schema.withProperties(name = "Bar") { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema).list()) }
val printedConfiguration = barConfigSchema.describe(configuration)
val printedConfiguration = barConfigSchema.describe(configuration, options = Configuration.Options.defaults)
val description = printedConfiguration.serialize().also { println(it) }

View File

@ -16,7 +16,7 @@ class SpecificationTest {
val principal by string().mapValid(::parseAddress)
val admin by string().mapValid(::parseAddress)
override fun parseValid(configuration: Config) = valid(Addresses(configuration[principal], configuration[admin]))
override fun parseValid(configuration: Config, options: Configuration.Options) = configuration.withOptions(options).let { valid(Addresses(it[principal], it[admin])) }
private fun parseAddress(rawValue: String): Valid<Address> {
@ -27,7 +27,7 @@ class SpecificationTest {
val useSsl by boolean()
val addresses by nested(AddressesSpec)
override fun parseValid(configuration: Config) = valid<RpcSettings>(RpcSettingsImpl(configuration[addresses], configuration[useSsl]))
override fun parseValid(configuration: Config, options: Configuration.Options) = configuration.withOptions(options).let { valid<RpcSettings>(RpcSettingsImpl(it[addresses], it[useSsl])) }
}
@Test(timeout=300_000)
@ -60,9 +60,9 @@ class SpecificationTest {
private val maxElement by long("elements").list().map { elements -> elements.max() }
override fun parseValid(configuration: Config): Valid<AtomicLong> {
return valid(AtomicLong(configuration[maxElement]!!))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<AtomicLong> {
val config = configuration.withOptions(options)
return valid(AtomicLong(config[maxElement]!!))
}
}
@ -111,9 +111,9 @@ class SpecificationTest {
private val maxElement by long("elements").list().mapValid(::parseMax)
override fun parseValid(configuration: Config): Valid<AtomicLong> {
return valid(AtomicLong(configuration[maxElement]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<AtomicLong> {
val config = configuration.withOptions(options)
return valid(AtomicLong(config[maxElement]))
}
}
@ -159,7 +159,7 @@ class SpecificationTest {
@Suppress("unused")
val myProp by string().list().optional()
override fun parseValid(configuration: Config) = valid(configuration[myProp])
override fun parseValid(configuration: Config, options: Configuration.Options) = configuration.withOptions(options).let { valid(it[myProp]) }
}
assertThat(spec.properties).hasSize(1)

View File

@ -15,5 +15,5 @@ internal fun <VALUE> extractValueWithErrors(errors: Set<Configuration.Validation
internal fun <VALUE> extractValue(value: Valid<VALUE>) = object : Configuration.Value.Parser<VALUE> {
override fun parse(configuration: Config, options: Configuration.Validation.Options): Valid<VALUE> = value
override fun parse(configuration: Config, options: Configuration.Options): Valid<VALUE> = value
}

View File

@ -30,7 +30,7 @@ snakeYamlVersion=1.19
caffeineVersion=2.7.0
metricsVersion=4.1.0
metricsNewRelicVersion=1.1.1
djvmVersion=1.0-RC10
djvmVersion=1.0
deterministicRtVersion=1.0-RC02
openSourceBranch=https://github.com/corda/corda/blob/release/os/4.4
openSourceSamplesBranch=https://github.com/corda/samples/blob/release-V4

View File

@ -23,7 +23,6 @@ import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import static net.corda.testing.driver.Driver.driver;
import static org.junit.Assert.assertEquals;
public class FlowExternalOperationInJavaTest extends AbstractFlowExternalOperationTest {
@ -32,16 +31,16 @@ public class FlowExternalOperationInJavaTest extends AbstractFlowExternalOperati
driver(new DriverParameters().withStartNodesInProcess(true), driver -> {
NodeHandle alice = KotlinUtilsKt.getOrThrow(
driver.startNode(new NodeParameters().withProvidedName(TestConstants.ALICE_NAME)),
Duration.of(20, ChronoUnit.SECONDS)
Duration.of(1, ChronoUnit.MINUTES)
);
NodeHandle bob = KotlinUtilsKt.getOrThrow(
driver.startNode(new NodeParameters().withProvidedName(TestConstants.BOB_NAME)),
Duration.of(20, ChronoUnit.SECONDS)
Duration.of(1, ChronoUnit.MINUTES)
);
return KotlinUtilsKt.getOrThrow(alice.getRpc().startFlowDynamic(
FlowWithExternalOperationInJava.class,
TestUtils.singleIdentity(bob.getNodeInfo())
).getReturnValue(), Duration.of(20, ChronoUnit.SECONDS));
).getReturnValue(), Duration.of(1, ChronoUnit.MINUTES));
});
}
@ -50,16 +49,16 @@ public class FlowExternalOperationInJavaTest extends AbstractFlowExternalOperati
driver(new DriverParameters().withStartNodesInProcess(true), driver -> {
NodeHandle alice = KotlinUtilsKt.getOrThrow(
driver.startNode(new NodeParameters().withProvidedName(TestConstants.ALICE_NAME)),
Duration.of(20, ChronoUnit.SECONDS)
Duration.of(1, ChronoUnit.MINUTES)
);
NodeHandle bob = KotlinUtilsKt.getOrThrow(
driver.startNode(new NodeParameters().withProvidedName(TestConstants.BOB_NAME)),
Duration.of(20, ChronoUnit.SECONDS)
Duration.of(1, ChronoUnit.MINUTES)
);
return KotlinUtilsKt.getOrThrow(alice.getRpc().startFlowDynamic(
FlowWithExternalAsyncOperationInJava.class,
TestUtils.singleIdentity(bob.getNodeInfo())
).getReturnValue(), Duration.of(20, ChronoUnit.SECONDS));
).getReturnValue(), Duration.of(1, ChronoUnit.MINUTES));
});
}
@ -68,22 +67,18 @@ public class FlowExternalOperationInJavaTest extends AbstractFlowExternalOperati
driver(new DriverParameters().withStartNodesInProcess(true), driver -> {
NodeHandle alice = KotlinUtilsKt.getOrThrow(
driver.startNode(new NodeParameters().withProvidedName(TestConstants.ALICE_NAME)),
Duration.of(20, ChronoUnit.SECONDS)
Duration.of(1, ChronoUnit.MINUTES)
);
NodeHandle bob = KotlinUtilsKt.getOrThrow(
driver.startNode(new NodeParameters().withProvidedName(TestConstants.BOB_NAME)),
Duration.of(20, ChronoUnit.SECONDS)
Duration.of(1, ChronoUnit.MINUTES)
);
KotlinUtilsKt.getOrThrow(alice.getRpc().startFlowDynamic(
FlowWithExternalOperationThatGetsRetriedInJava.class,
TestUtils.singleIdentity(bob.getNodeInfo())
).getReturnValue(), Duration.of(20, ChronoUnit.SECONDS));
).getReturnValue(), Duration.of(1, ChronoUnit.MINUTES));
HospitalCounts counts = KotlinUtilsKt.getOrThrow(alice.getRpc().startFlowDynamic(
GetHospitalCountersFlow.class
).getReturnValue(), Duration.of(20, ChronoUnit.SECONDS));
assertEquals(1, counts.getDischarge());
assertEquals(0, counts.getObservation());
assertHospitalCounters(1, 0);
return null;
});

View File

@ -12,26 +12,54 @@ import net.corda.core.flows.StartableByRPC
import net.corda.core.flows.StartableByService
import net.corda.core.identity.Party
import net.corda.core.internal.concurrent.doOnComplete
import net.corda.core.messaging.FlowHandle
import net.corda.core.node.AppServiceHub
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.CordaService
import net.corda.core.schemas.MappedSchema
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.getOrThrow
import net.corda.node.services.statemachine.StaffedFlowHospital
import org.junit.Before
import java.sql.SQLTransientConnectionException
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
import java.util.concurrent.Semaphore
import java.util.function.Supplier
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.Table
import kotlin.test.assertEquals
abstract class AbstractFlowExternalOperationTest {
var dischargeCounter = 0
var observationCounter = 0
@Before
fun before() {
StaffedFlowHospital.onFlowDischarged.clear()
StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++dischargeCounter }
StaffedFlowHospital.onFlowKeptForOvernightObservation.clear()
StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ -> ++observationCounter }
dischargeCounter = 0
observationCounter = 0
}
fun blockUntilFlowKeptInForObservation(flow: () -> FlowHandle<*>) {
val lock = Semaphore(0)
StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ -> lock.release() }
flow()
lock.acquire()
}
fun assertHospitalCounters(discharge: Int, observation: Int) {
assertEquals(discharge, dischargeCounter)
assertEquals(observation, observationCounter)
}
@StartableByRPC
@InitiatingFlow
@StartableByService
@ -182,31 +210,6 @@ abstract class AbstractFlowExternalOperationTest {
object CustomMappedSchema : MappedSchema(CustomSchema::class.java, 1, listOf(CustomTableEntity::class.java))
// Internal use for testing only!!
@StartableByRPC
class GetHospitalCountersFlow : FlowLogic<HospitalCounts>() {
override fun call(): HospitalCounts =
HospitalCounts(
serviceHub.cordaService(HospitalCounter::class.java).dischargeCounter,
serviceHub.cordaService(HospitalCounter::class.java).observationCounter
)
}
@CordaSerializable
data class HospitalCounts(val discharge: Int, val observation: Int)
@Suppress("UNUSED_PARAMETER")
@CordaService
class HospitalCounter(services: AppServiceHub) : SingletonSerializeAsToken() {
var observationCounter: Int = 0
var dischargeCounter: Int = 0
init {
StaffedFlowHospital.onFlowDischarged.add { _, _ -> ++dischargeCounter }
StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ -> ++observationCounter }
}
}
class MyCordaException(message: String) : CordaException(message)
class DirectlyAccessedServiceHubException : CordaException("Null pointer from accessing flow's serviceHub")

View File

@ -6,12 +6,7 @@ import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds
import net.corda.coretests.flows.AbstractFlowExternalOperationTest.DirectlyAccessedServiceHubException
import net.corda.coretests.flows.AbstractFlowExternalOperationTest.ExternalAsyncOperation
import net.corda.coretests.flows.AbstractFlowExternalOperationTest.FlowWithExternalProcess
import net.corda.coretests.flows.AbstractFlowExternalOperationTest.FutureService
import net.corda.coretests.flows.AbstractFlowExternalOperationTest.MyCordaException
import net.corda.core.utilities.minutes
import net.corda.node.services.statemachine.StateTransitionException
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
@ -21,28 +16,24 @@ import net.corda.testing.driver.driver
import org.junit.Test
import java.sql.SQLTransientConnectionException
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeoutException
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class FlowExternalAsyncOperationTest : AbstractFlowExternalOperationTest() {
@Test(timeout=300_000)
fun `external async operation`() {
@Test(timeout = 300_000)
fun `external async operation`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
alice.rpc.startFlow(::FlowWithExternalAsyncOperation, bob.nodeInfo.singleIdentity())
.returnValue.getOrThrow(20.seconds)
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(0, discharged)
assertEquals(0, observation)
.returnValue.getOrThrow(1.minutes)
assertHospitalCounters(0, 0)
}
}
@Test(timeout=300_000)
fun `external async operation that checks deduplicationId is not rerun when flow is retried`() {
@Test(timeout = 300_000)
fun `external async operation that checks deduplicationId is not rerun when flow is retried`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
@ -50,16 +41,14 @@ class FlowExternalAsyncOperationTest : AbstractFlowExternalOperationTest() {
alice.rpc.startFlow(
::FlowWithExternalAsyncOperationWithDeduplication,
bob.nodeInfo.singleIdentity()
).returnValue.getOrThrow(20.seconds)
).returnValue.getOrThrow(1.minutes)
}
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(1, discharged)
assertEquals(0, observation)
assertHospitalCounters(1, 0)
}
}
@Test(timeout=300_000)
fun `external async operation propagates exception to calling flow`() {
@Test(timeout = 300_000)
fun `external async operation propagates exception to calling flow`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
@ -68,100 +57,88 @@ class FlowExternalAsyncOperationTest : AbstractFlowExternalOperationTest() {
::FlowWithExternalAsyncOperationPropagatesException,
bob.nodeInfo.singleIdentity(),
MyCordaException::class.java
).returnValue.getOrThrow(20.seconds)
).returnValue.getOrThrow(1.minutes)
}
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(0, discharged)
assertEquals(0, observation)
assertHospitalCounters(0, 0)
}
}
@Test(timeout=300_000)
fun `external async operation exception can be caught in flow`() {
@Test(timeout = 300_000)
fun `external async operation exception can be caught in flow`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
val result = alice.rpc.startFlow(
::FlowWithExternalAsyncOperationThatThrowsExceptionAndCaughtInFlow,
bob.nodeInfo.singleIdentity()
).returnValue.getOrThrow(20.seconds)
).returnValue.getOrThrow(1.minutes)
assertTrue(result as Boolean)
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(0, discharged)
assertEquals(0, observation)
assertHospitalCounters(0, 0)
}
}
@Test(timeout=300_000)
fun `external async operation with exception that hospital keeps for observation does not fail`() {
@Test(timeout = 300_000)
fun `external async operation with exception that hospital keeps for observation does not fail`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
assertFailsWith<TimeoutException> {
blockUntilFlowKeptInForObservation {
alice.rpc.startFlow(
::FlowWithExternalAsyncOperationPropagatesException,
bob.nodeInfo.singleIdentity(),
HospitalizeFlowException::class.java
).returnValue.getOrThrow(20.seconds)
)
}
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(0, discharged)
assertEquals(1, observation)
assertHospitalCounters(0, 1)
}
}
@Test(timeout=300_000)
fun `external async operation with exception that hospital discharges is retried and runs the future again`() {
@Test(timeout = 300_000)
fun `external async operation with exception that hospital discharges is retried and runs the future again`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
assertFailsWith<TimeoutException> {
blockUntilFlowKeptInForObservation {
alice.rpc.startFlow(
::FlowWithExternalAsyncOperationPropagatesException,
bob.nodeInfo.singleIdentity(),
SQLTransientConnectionException::class.java
).returnValue.getOrThrow(20.seconds)
)
}
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(3, discharged)
assertEquals(1, observation)
assertHospitalCounters(3, 1)
}
}
@Test(timeout=300_000)
fun `external async operation that throws exception rather than completing future exceptionally fails with internal exception`() {
@Test(timeout = 300_000)
fun `external async operation that throws exception rather than completing future exceptionally fails with internal exception`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
assertFailsWith<StateTransitionException> {
alice.rpc.startFlow(::FlowWithExternalAsyncOperationUnhandledException, bob.nodeInfo.singleIdentity())
.returnValue.getOrThrow(20.seconds)
.returnValue.getOrThrow(1.minutes)
}
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(0, discharged)
assertEquals(0, observation)
assertHospitalCounters(0, 0)
}
}
@Test(timeout=300_000)
fun `external async operation that passes serviceHub into process can be retried`() {
@Test(timeout = 300_000)
fun `external async operation that passes serviceHub into process can be retried`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
assertFailsWith<TimeoutException> {
blockUntilFlowKeptInForObservation {
alice.rpc.startFlow(
::FlowWithExternalAsyncOperationThatPassesInServiceHubCanRetry,
bob.nodeInfo.singleIdentity()
).returnValue.getOrThrow(20.seconds)
)
}
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(3, discharged)
assertEquals(1, observation)
assertHospitalCounters(3, 1)
}
}
@Test(timeout=300_000)
fun `external async operation that accesses serviceHub from flow directly will fail when retried`() {
@Test(timeout = 300_000)
fun `external async operation that accesses serviceHub from flow directly will fail when retried`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
@ -169,23 +146,19 @@ class FlowExternalAsyncOperationTest : AbstractFlowExternalOperationTest() {
alice.rpc.startFlow(
::FlowWithExternalAsyncOperationThatDirectlyAccessesServiceHubFailsRetry,
bob.nodeInfo.singleIdentity()
).returnValue.getOrThrow(20.seconds)
).returnValue.getOrThrow(1.minutes)
}
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(1, discharged)
assertEquals(0, observation)
assertHospitalCounters(1, 0)
}
}
@Test(timeout=300_000)
fun `starting multiple futures and joining on their results`() {
@Test(timeout = 300_000)
fun `starting multiple futures and joining on their results`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
alice.rpc.startFlow(::FlowThatStartsMultipleFuturesAndJoins, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow(20.seconds)
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(0, discharged)
assertEquals(0, observation)
alice.rpc.startFlow(::FlowThatStartsMultipleFuturesAndJoins, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow(1.minutes)
assertHospitalCounters(0, 0)
}
}

View File

@ -5,40 +5,35 @@ import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds
import net.corda.core.utilities.minutes
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.singleIdentity
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import org.junit.Test
import kotlin.test.assertEquals
class FlowExternalOperationStartFlowTest : AbstractFlowExternalOperationTest() {
@Test(timeout=300_000)
fun `starting a flow inside of a flow that starts a future will succeed`() {
@Test(timeout = 300_000)
fun `starting a flow inside of a flow that starts a future will succeed`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
alice.rpc.startFlow(::FlowThatStartsAnotherFlowInAnExternalOperation, bob.nodeInfo.singleIdentity())
.returnValue.getOrThrow(40.seconds)
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(0, discharged)
assertEquals(0, observation)
.returnValue.getOrThrow(1.minutes)
assertHospitalCounters(0, 0)
}
}
@Test(timeout=300_000)
fun `multiple flows can be started and their futures joined from inside a flow`() {
@Test(timeout = 300_000)
fun `multiple flows can be started and their futures joined from inside a flow`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
alice.rpc.startFlow(::ForkJoinFlows, bob.nodeInfo.singleIdentity())
.returnValue.getOrThrow(40.seconds)
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(0, discharged)
assertEquals(0, observation)
.returnValue.getOrThrow(1.minutes)
assertHospitalCounters(0, 0)
}
}

View File

@ -10,13 +10,8 @@ import net.corda.core.messaging.startFlow
import net.corda.core.node.services.queryBy
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds
import net.corda.coretests.flows.AbstractFlowExternalOperationTest.CustomTableEntity
import net.corda.coretests.flows.AbstractFlowExternalOperationTest.DirectlyAccessedServiceHubException
import net.corda.coretests.flows.AbstractFlowExternalOperationTest.ExternalOperation
import net.corda.coretests.flows.AbstractFlowExternalOperationTest.FlowWithExternalProcess
import net.corda.coretests.flows.AbstractFlowExternalOperationTest.FutureService
import net.corda.coretests.flows.AbstractFlowExternalOperationTest.MyCordaException
import net.corda.core.utilities.minutes
import net.corda.node.services.statemachine.StaffedFlowHospital
import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyState
import net.corda.testing.core.ALICE_NAME
@ -26,30 +21,26 @@ import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.node.internal.cordappsForPackages
import org.junit.Test
import java.lang.IllegalStateException
import java.sql.SQLTransientConnectionException
import java.util.concurrent.TimeoutException
import kotlin.test.assertEquals
import java.util.concurrent.Semaphore
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class FlowExternalOperationTest : AbstractFlowExternalOperationTest() {
@Test(timeout=300_000)
fun `external operation`() {
@Test(timeout = 300_000)
fun `external operation`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
alice.rpc.startFlow(::FlowWithExternalOperation, bob.nodeInfo.singleIdentity())
.returnValue.getOrThrow(20.seconds)
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(0, discharged)
assertEquals(0, observation)
.returnValue.getOrThrow(1.minutes)
assertHospitalCounters(0, 0)
}
}
@Test(timeout=300_000)
fun `external operation that checks deduplicationId is not rerun when flow is retried`() {
@Test(timeout = 300_000)
fun `external operation that checks deduplicationId is not rerun when flow is retried`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
@ -57,16 +48,14 @@ class FlowExternalOperationTest : AbstractFlowExternalOperationTest() {
alice.rpc.startFlow(
::FlowWithExternalOperationWithDeduplication,
bob.nodeInfo.singleIdentity()
).returnValue.getOrThrow(20.seconds)
).returnValue.getOrThrow(1.minutes)
}
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(1, discharged)
assertEquals(0, observation)
assertHospitalCounters(1, 0)
}
}
@Test(timeout=300_000)
fun `external operation propagates exception to calling flow`() {
@Test(timeout = 300_000)
fun `external operation propagates exception to calling flow`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
@ -75,82 +64,72 @@ class FlowExternalOperationTest : AbstractFlowExternalOperationTest() {
::FlowWithExternalOperationPropagatesException,
bob.nodeInfo.singleIdentity(),
MyCordaException::class.java
).returnValue.getOrThrow(20.seconds)
).returnValue.getOrThrow(1.minutes)
}
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(0, discharged)
assertEquals(0, observation)
assertHospitalCounters(0, 0)
}
}
@Test(timeout=300_000)
fun `external operation exception can be caught in flow`() {
@Test(timeout = 300_000)
fun `external operation exception can be caught in flow`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
alice.rpc.startFlow(::FlowWithExternalOperationThatThrowsExceptionAndCaughtInFlow, bob.nodeInfo.singleIdentity())
.returnValue.getOrThrow(20.seconds)
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(0, discharged)
assertEquals(0, observation)
.returnValue.getOrThrow(1.minutes)
assertHospitalCounters(0, 0)
}
}
@Test(timeout=300_000)
fun `external operation with exception that hospital keeps for observation does not fail`() {
@Test(timeout = 300_000)
fun `external operation with exception that hospital keeps for observation does not fail`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
assertFailsWith<TimeoutException> {
blockUntilFlowKeptInForObservation {
alice.rpc.startFlow(
::FlowWithExternalOperationPropagatesException,
bob.nodeInfo.singleIdentity(),
HospitalizeFlowException::class.java
).returnValue.getOrThrow(20.seconds)
)
}
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(0, discharged)
assertEquals(1, observation)
assertHospitalCounters(0, 1)
}
}
@Test(timeout=300_000)
fun `external operation with exception that hospital discharges is retried and runs the external operation again`() {
@Test(timeout = 300_000)
fun `external operation with exception that hospital discharges is retried and runs the external operation again`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
assertFailsWith<TimeoutException> {
blockUntilFlowKeptInForObservation {
alice.rpc.startFlow(
::FlowWithExternalOperationPropagatesException,
bob.nodeInfo.singleIdentity(),
SQLTransientConnectionException::class.java
).returnValue.getOrThrow(20.seconds)
)
}
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(3, discharged)
assertEquals(1, observation)
assertHospitalCounters(3, 1)
}
}
@Test(timeout=300_000)
fun `external async operation that passes serviceHub into process can be retried`() {
@Test(timeout = 300_000)
fun `external async operation that passes serviceHub into process can be retried`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
assertFailsWith<TimeoutException> {
blockUntilFlowKeptInForObservation {
alice.rpc.startFlow(
::FlowWithExternalOperationThatPassesInServiceHubCanRetry,
bob.nodeInfo.singleIdentity()
).returnValue.getOrThrow(20.seconds)
)
}
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(3, discharged)
assertEquals(1, observation)
assertHospitalCounters(3, 1)
}
}
@Test(timeout=300_000)
fun `external async operation that accesses serviceHub from flow directly will fail when retried`() {
@Test(timeout = 300_000)
fun `external async operation that accesses serviceHub from flow directly will fail when retried`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
@ -158,16 +137,14 @@ class FlowExternalOperationTest : AbstractFlowExternalOperationTest() {
alice.rpc.startFlow(
::FlowWithExternalOperationThatDirectlyAccessesServiceHubFailsRetry,
bob.nodeInfo.singleIdentity()
).returnValue.getOrThrow(20.seconds)
).returnValue.getOrThrow(1.minutes)
}
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(1, discharged)
assertEquals(0, observation)
assertHospitalCounters(1, 0)
}
}
@Test(timeout=300_000)
fun `vault can be queried`() {
@Test(timeout = 300_000)
fun `vault can be queried`() {
driver(
DriverParameters(
cordappsForAllNodes = cordappsForPackages(DummyState::class.packageName),
@ -176,64 +153,62 @@ class FlowExternalOperationTest : AbstractFlowExternalOperationTest() {
) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val success = alice.rpc.startFlow(::FlowWithWithExternalOperationThatQueriesVault)
.returnValue.getOrThrow(20.seconds)
.returnValue.getOrThrow(1.minutes)
assertTrue(success)
}
}
@Test(timeout=300_000)
fun `data can be persisted to node database via entity manager`() {
@Test(timeout = 300_000)
fun `data can be persisted to node database via entity manager`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val success = alice.rpc.startFlow(::FlowWithExternalOperationThatPersistsViaEntityManager)
.returnValue.getOrThrow(20.seconds)
.returnValue.getOrThrow(1.minutes)
assertTrue(success)
}
}
@Test(timeout=300_000)
fun `data can be persisted to node database via jdbc session`() {
@Test(timeout = 300_000)
fun `data can be persisted to node database via jdbc session`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val success = alice.rpc.startFlow(::FlowWithExternalOperationThatPersistsViaJdbcSession)
.returnValue.getOrThrow(20.seconds)
.returnValue.getOrThrow(1.minutes)
assertTrue(success)
}
}
@Test(timeout=300_000)
fun `data can be persisted to node database via servicehub database transaction`() {
@Test(timeout = 300_000)
fun `data can be persisted to node database via servicehub database transaction`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val success = alice.rpc.startFlow(::FlowWithExternalOperationThatPersistsViaDatabaseTransaction)
.returnValue.getOrThrow(20.seconds)
.returnValue.getOrThrow(1.minutes)
assertTrue(success)
}
}
@Test(timeout=300_000)
fun `data can be persisted to node database in external operation and read from another process once finished`() {
@Test(timeout = 300_000)
fun `data can be persisted to node database in external operation and read from another process once finished`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val success = alice.rpc.startFlow(::FlowWithExternalOperationThatPersistsToDatabaseAndReadsFromExternalOperation)
.returnValue.getOrThrow(20.seconds)
.returnValue.getOrThrow(1.minutes)
assertTrue(success)
}
}
@Test(timeout=300_000)
fun `external operation can be retried when an error occurs inside of database transaction`() {
@Test(timeout = 300_000)
fun `external operation can be retried when an error occurs inside of database transaction`() {
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val bob = startNode(providedName = BOB_NAME).getOrThrow()
val success = alice.rpc.startFlow(
::FlowWithExternalOperationThatErrorsInsideOfDatabaseTransaction,
bob.nodeInfo.singleIdentity()
).returnValue.getOrThrow(20.seconds)
).returnValue.getOrThrow(1.minutes)
assertTrue(success as Boolean)
val (discharged, observation) = alice.rpc.startFlow(::GetHospitalCountersFlow).returnValue.getOrThrow()
assertEquals(1, discharged)
assertEquals(0, observation)
assertHospitalCounters(1, 0)
}
}

View File

@ -21,14 +21,18 @@ import net.corda.testing.internal.services.InternalMockAttachmentStorage
import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP
import net.corda.testing.services.MockAttachmentStorage
import org.apache.commons.io.IOUtils
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.net.URL
import kotlin.test.assertFailsWith
import kotlin.test.fail
class AttachmentsClassLoaderTests {
companion object {
@ -84,14 +88,29 @@ class AttachmentsClassLoaderTests {
}
}
@Test(timeout=300_000)
fun `test contracts have no permissions for protection domain`() {
val isolatedId = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
assertNull(System.getSecurityManager())
createClassloader(isolatedId).use { classLoader ->
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classLoader)
val protectionDomain = contractClass.protectionDomain ?: fail("Protection Domain missing")
val permissions = protectionDomain.permissions ?: fail("Protection domain has no permissions")
assertThat(permissions.elements().toList()).isEmpty()
assertTrue(permissions.isReadOnly)
}
}
@Test(timeout=300_000)
fun `Dynamically load AnotherDummyContract from isolated contracts jar using the AttachmentsClassLoader`() {
val isolatedId = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
val classloader = createClassloader(isolatedId)
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classloader)
val contract = contractClass.getDeclaredConstructor().newInstance() as Contract
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
createClassloader(isolatedId).use { classloader ->
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classloader)
val contract = contractClass.getDeclaredConstructor().newInstance() as Contract
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
}
}
@Test(timeout=300_000)
@ -100,7 +119,7 @@ class AttachmentsClassLoaderTests {
val att2 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH_V4.openStream(), "app", "isolated-4.0.jar")
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
createClassloader(listOf(att1, att2))
createClassloader(listOf(att1, att2)).use {}
}
}
@ -111,7 +130,7 @@ class AttachmentsClassLoaderTests {
val isolatedSignedId = importAttachment(signedJar.first.toUri().toURL().openStream(), "app", "isolated-signed.jar")
// does not throw OverlappingAttachments exception
createClassloader(listOf(isolatedId, isolatedSignedId))
createClassloader(listOf(isolatedId, isolatedSignedId)).use {}
}
@Test(timeout=300_000)
@ -120,7 +139,7 @@ class AttachmentsClassLoaderTests {
val att2 = importAttachment(FINANCE_CONTRACTS_CORDAPP.jarFile.inputStream(), "app", "finance.jar")
// does not throw OverlappingAttachments exception
createClassloader(listOf(att1, att2))
createClassloader(listOf(att1, att2)).use {}
}
@Test(timeout=300_000)
@ -128,12 +147,13 @@ class AttachmentsClassLoaderTests {
val att1 = importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar")
val cl = createClassloader(listOf(att1, att2))
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
assertEquals("some data", txt)
createClassloader(listOf(att1, att2)).use { cl ->
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
assertEquals("some data", txt)
val txt1 = IOUtils.toString(cl.getResourceAsStream("file2.txt"), Charsets.UTF_8.name())
assertEquals("some other data", txt1)
val txt1 = IOUtils.toString(cl.getResourceAsStream("file2.txt"), Charsets.UTF_8.name())
assertEquals("some other data", txt1)
}
}
@Test(timeout=300_000)
@ -141,9 +161,10 @@ class AttachmentsClassLoaderTests {
val att1 = importAttachment(fakeAttachment("file1.txt", "same data", "file2.txt", "same other data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment("file1.txt", "same data", "file3.txt", "same totally different").inputStream(), "app", "file2.jar")
val cl = createClassloader(listOf(att1, att2))
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
assertEquals("same data", txt)
createClassloader(listOf(att1, att2)).use { cl ->
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
assertEquals("same data", txt)
}
}
@Test(timeout=300_000)
@ -152,7 +173,7 @@ class AttachmentsClassLoaderTests {
val att1 = importAttachment(fakeAttachment(path, "some data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment(path, "some other data").inputStream(), "app", "file2.jar")
createClassloader(listOf(att1, att2))
createClassloader(listOf(att1, att2)).use {}
}
}
@ -161,7 +182,7 @@ class AttachmentsClassLoaderTests {
val att1 = importAttachment(fakeAttachment("meta-inf/services/net.corda.core.serialization.SerializationWhitelist", "some data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment("meta-inf/services/net.corda.core.serialization.SerializationWhitelist", "some other data").inputStream(), "app", "file2.jar")
createClassloader(listOf(att1, att2))
createClassloader(listOf(att1, att2)).use {}
}
@Test(timeout=300_000)
@ -170,7 +191,7 @@ class AttachmentsClassLoaderTests {
val att2 = importAttachment(fakeAttachment("meta-inf/services/com.example.something", "some other data").inputStream(), "app", "file2.jar")
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
createClassloader(listOf(att1, att2))
createClassloader(listOf(att1, att2)).use {}
}
}
@ -180,7 +201,7 @@ class AttachmentsClassLoaderTests {
val att2 = storage.importAttachment(fakeAttachment("file1.txt", "some other data").inputStream(), "app", "file2.jar")
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
createClassloader(listOf(att1, att2))
createClassloader(listOf(att1, att2)).use {}
}
}
@ -191,7 +212,7 @@ class AttachmentsClassLoaderTests {
val att1 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", ISOLATED_CONTRACTS_JAR_PATH.file)
val att2 = importAttachment(fakeAttachment("net/corda/finance/contracts/isolated/AnotherDummyContract\$State.class", "some attackdata").inputStream(), "app", "file2.jar")
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
createClassloader(listOf(att1, att2))
createClassloader(listOf(att1, att2)).use {}
}
}
@ -220,10 +241,10 @@ class AttachmentsClassLoaderTests {
val untrustedClassJar = importAttachment(fakeAttachment("/com/example/something/MaliciousClass.class", "some malicious data").inputStream(), "untrusted", "file2.jar")
val trustedClassJar = importAttachment(fakeAttachment("/com/example/something/VirtuousClass.class", "some other data").inputStream(), "app", "file3.jar")
createClassloader(listOf(trustedResourceJar, untrustedResourceJar, trustedClassJar))
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
createClassloader(listOf(trustedResourceJar, untrustedResourceJar, trustedClassJar, untrustedClassJar))
createClassloader(listOf(trustedResourceJar, untrustedResourceJar, trustedClassJar)).use {
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
createClassloader(listOf(trustedResourceJar, untrustedResourceJar, trustedClassJar, untrustedClassJar)).use {}
}
}
}
@ -257,7 +278,7 @@ class AttachmentsClassLoaderTests {
signers = listOf(keyPairA.public, keyPairB.public)
)
createClassloader(untrustedAttachment)
createClassloader(untrustedAttachment).use {}
}
@Test(timeout=300_000)
@ -287,7 +308,7 @@ class AttachmentsClassLoaderTests {
signers = listOf(keyPairA.public, keyPairB.public)
)
createClassloader(untrustedAttachment)
createClassloader(untrustedAttachment).use {}
}
@Test(timeout=300_000)
@ -306,7 +327,7 @@ class AttachmentsClassLoaderTests {
)
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
createClassloader(untrustedAttachment)
createClassloader(untrustedAttachment).use {}
}
}
@ -337,7 +358,7 @@ class AttachmentsClassLoaderTests {
)
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
createClassloader(untrustedAttachment)
createClassloader(untrustedAttachment).use {}
}
}
@ -380,10 +401,10 @@ class AttachmentsClassLoaderTests {
)
// pass the inherited trust attachment through the classloader first to ensure it does not affect the next loaded attachment
createClassloader(inheritedTrustAttachment)
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
createClassloader(untrustedAttachment)
createClassloader(inheritedTrustAttachment).use {
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
createClassloader(untrustedAttachment).use {}
}
}
}
@ -421,7 +442,7 @@ class AttachmentsClassLoaderTests {
)
assertFailsWith(TransactionVerificationException.UntrustedAttachmentsException::class) {
createClassloader(untrustedAttachment)
createClassloader(untrustedAttachment).use {}
}
}
@ -446,6 +467,6 @@ class AttachmentsClassLoaderTests {
signers = listOf(keyPairA.public)
)
createClassloader(trustedAttachment)
createClassloader(trustedAttachment).use {}
}
}

View File

@ -333,7 +333,7 @@ abstract class FlowLogic<out T> {
@JvmOverloads
fun sendAll(payload: Any, sessions: Set<FlowSession>, maySkipCheckpoint: Boolean = false) {
val sessionToPayload = sessions.map { it to payload }.toMap()
return sendAll(sessionToPayload, maySkipCheckpoint)
return sendAllMap(sessionToPayload, maySkipCheckpoint)
}
/**
@ -348,7 +348,7 @@ abstract class FlowLogic<out T> {
*/
@Suspendable
@JvmOverloads
fun sendAll(payloadsPerSession: Map<FlowSession, Any>, maySkipCheckpoint: Boolean = false) {
fun sendAllMap(payloadsPerSession: Map<FlowSession, Any>, maySkipCheckpoint: Boolean = false) {
val request = FlowIORequest.Send(
sessionToMessage = serializePayloads(payloadsPerSession)
)

View File

@ -4,6 +4,7 @@ package net.corda.core.internal
import net.corda.core.DeleteForDJVM
import net.corda.core.KeepForDJVM
import net.corda.core.StubOutForDJVM
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
@ -417,6 +418,7 @@ fun <T, U : T> uncheckedCast(obj: T) = obj as U
fun <K, V> Iterable<Pair<K, V>>.toMultiMap(): Map<K, List<V>> = this.groupBy({ it.first }) { it.second }
/** Returns the location of this class. */
@get:StubOutForDJVM
val Class<*>.location: URL get() = protectionDomain.codeSource.location
/** Convenience method to get the package name of a class literal. */

View File

@ -302,7 +302,7 @@ object Builder {
@JvmStatic
@JvmOverloads
fun <R> FieldInfo.notEqual(value: R, exactMatch: Boolean = true) = predicate(Builder.equal(value, exactMatch))
fun <R> FieldInfo.notEqual(value: R, exactMatch: Boolean = true) = predicate(Builder.notEqual(value, exactMatch))
@JvmStatic
@Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.")

View File

@ -18,6 +18,7 @@ import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
import java.net.*
import java.security.Permission
import java.util.*
/**
@ -378,6 +379,15 @@ object AttachmentURLStreamHandlerFactory : URLStreamHandlerFactory {
private class AttachmentURLConnection(url: URL, private val attachment: Attachment) : URLConnection(url) {
override fun getContentLengthLong(): Long = attachment.size.toLong()
override fun getInputStream(): InputStream = attachment.open()
/**
* Define the permissions that [AttachmentsClassLoader] will need to
* use this [URL]. The attachment is stored in memory, and so we
* don't need any extra permissions here. But if we don't override
* [getPermission] then [AttachmentsClassLoader] will assign the
* default permission of ALL_PERMISSION to these classes'
* [java.security.ProtectionDomain]. This would be a security hole!
*/
override fun getPermission(): Permission? = null
override fun connect() {
connected = true
}

View File

@ -0,0 +1,165 @@
package net.corda.core.node.services.vault
import net.corda.core.node.services.vault.BinaryComparisonOperator
import net.corda.core.node.services.vault.Builder.`in`
import net.corda.core.node.services.vault.Builder.equal
import net.corda.core.node.services.vault.Builder.greaterThan
import net.corda.core.node.services.vault.Builder.greaterThanOrEqual
import net.corda.core.node.services.vault.Builder.isNull
import net.corda.core.node.services.vault.Builder.lessThan
import net.corda.core.node.services.vault.Builder.lessThanOrEqual
import net.corda.core.node.services.vault.Builder.like
import net.corda.core.node.services.vault.Builder.notEqual
import net.corda.core.node.services.vault.Builder.notIn
import net.corda.core.node.services.vault.Builder.notLike
import net.corda.core.node.services.vault.Builder.notNull
import net.corda.core.node.services.vault.CollectionOperator
import net.corda.core.node.services.vault.ColumnPredicate
import net.corda.core.node.services.vault.ColumnPredicate.AggregateFunction
import net.corda.core.node.services.vault.ColumnPredicate.Between
import net.corda.core.node.services.vault.ColumnPredicate.BinaryComparison
import net.corda.core.node.services.vault.ColumnPredicate.CollectionExpression
import net.corda.core.node.services.vault.ColumnPredicate.EqualityComparison
import net.corda.core.node.services.vault.ColumnPredicate.Likeness
import net.corda.core.node.services.vault.ColumnPredicate.NullExpression
import net.corda.core.node.services.vault.CriteriaExpression.ColumnPredicateExpression
import net.corda.core.node.services.vault.EqualityComparisonOperator
import net.corda.core.node.services.vault.FieldInfo
import net.corda.core.node.services.vault.LikenessOperator
import net.corda.core.node.services.vault.NullOperator
import net.corda.core.node.services.vault.Operator
import net.corda.core.node.services.vault.getField
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.ObjectAssert
import org.junit.Test
import javax.persistence.Entity
class QueryCriteriaUtilsBuilderTest {
/** JPA Entity class needed by `getField` */
@Entity
private class TestEntity(val field: String)
/** Returns a `FieldInfo` object to work on */
private val fieldInfo: FieldInfo get() = getField("field", TestEntity::class.java)
/** Thrown for the `ColumnPredicate` types that have no `operator` field */
private class ColumnPredicateHasNoOperatorFieldException : Exception("This ColumnPredicate has no operator field")
/** Returns the `operator` for the given `ColumnPredicate` */
private fun ColumnPredicate<out Any?>.getOperator(): Operator = when (this) {
is AggregateFunction -> throw ColumnPredicateHasNoOperatorFieldException()
is Between -> throw ColumnPredicateHasNoOperatorFieldException()
is BinaryComparison<*> -> operator
is CollectionExpression -> operator
is EqualityComparison<*> -> operator
is Likeness -> operator
is NullExpression -> operator
}
/** Returns the `operator` for the given `ColumnPredicateExpression` */
private fun ColumnPredicateExpression<Any, *>.getOperator(): Operator = this.predicate.getOperator()
/** Assert that the `ColumnPredicateExpression` uses the given `Operator`. */
private fun <T : ColumnPredicateExpression<Any, C>, C> ObjectAssert<T>.usesOperator(operator: Operator) {
extracting {
assertThat(it.getOperator()).isEqualTo(operator)
}
}
/** Sample `String` value to pass to the predicate expression */
private val stringValue = ""
/** Sample `List` value to pass to the predicate expression */
private val listValue = emptyList<String>()
@Test(timeout = 500)
fun `equal predicate uses EQUAL operator`() {
assertThat(fieldInfo.equal(stringValue)).usesOperator(EqualityComparisonOperator.EQUAL)
}
@Test(timeout = 500)
fun `equal predicate (exactMatch=false) uses EQUAL_IGNORE_CASE operator`() {
assertThat(fieldInfo.equal(stringValue, exactMatch = false)).usesOperator(EqualityComparisonOperator.EQUAL_IGNORE_CASE)
}
@Test(timeout = 500)
fun `notEqual predicate uses NOT_EQUAL operator`() {
assertThat(fieldInfo.notEqual(stringValue)).usesOperator(EqualityComparisonOperator.NOT_EQUAL)
}
@Test(timeout = 500)
fun `notEqual predicate (exactMatch=false) uses NOT_EQUAL_IGNORE_CASE operator`() {
assertThat(fieldInfo.notEqual(stringValue, exactMatch = false)).usesOperator(EqualityComparisonOperator.NOT_EQUAL_IGNORE_CASE)
}
@Test(timeout = 500)
fun `lessThan predicate uses LESS_THAN operator`() {
assertThat(fieldInfo.lessThan(stringValue)).usesOperator(BinaryComparisonOperator.LESS_THAN)
}
@Test(timeout = 500)
fun `lessThanOrEqual predicate uses LESS_THAN_OR_EQUAL operator`() {
assertThat(fieldInfo.lessThanOrEqual(stringValue)).usesOperator(BinaryComparisonOperator.LESS_THAN_OR_EQUAL)
}
@Test(timeout = 500)
fun `greaterThan predicate uses GREATER_THAN operator`() {
assertThat(fieldInfo.greaterThan(stringValue)).usesOperator(BinaryComparisonOperator.GREATER_THAN)
}
@Test(timeout = 500)
fun `greaterThanOrEqual predicate uses GREATER_THAN_OR_EQUAL operator`() {
assertThat(fieldInfo.greaterThanOrEqual(stringValue)).usesOperator(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL)
}
@Test(timeout = 500)
fun `in predicate uses IN operator`() {
assertThat(fieldInfo.`in`(listValue)).usesOperator(CollectionOperator.IN)
}
@Test(timeout = 500)
fun `in predicate (exactMatch=false) uses IN_IGNORE_CASE operator`() {
assertThat(fieldInfo.`in`(listValue, exactMatch = false)).usesOperator(CollectionOperator.IN_IGNORE_CASE)
}
@Test(timeout = 500)
fun `notIn predicate uses NOT_IN operator`() {
assertThat(fieldInfo.notIn(listValue)).usesOperator(CollectionOperator.NOT_IN)
}
@Test(timeout = 500)
fun `notIn predicate (exactMatch=false) uses NOT_IN_IGNORE_CASE operator`() {
assertThat(fieldInfo.notIn(listValue, exactMatch = false)).usesOperator(CollectionOperator.NOT_IN_IGNORE_CASE)
}
@Test(timeout = 500)
fun `like predicate uses LIKE operator`() {
assertThat(fieldInfo.like(stringValue)).usesOperator(LikenessOperator.LIKE)
}
@Test(timeout = 500)
fun `like predicate (exactMatch=false) uses LIKE_IGNORE_CASE operator`() {
assertThat(fieldInfo.like(stringValue, exactMatch = false)).usesOperator(LikenessOperator.LIKE_IGNORE_CASE)
}
@Test(timeout = 500)
fun `notLike predicate uses NOT_LIKE operator`() {
assertThat(fieldInfo.notLike(stringValue)).usesOperator(LikenessOperator.NOT_LIKE)
}
@Test(timeout = 500)
fun `notLike predicate (exactMatch=false) uses NOT_LIKE_IGNORE_CASE operator`() {
assertThat(fieldInfo.notLike(stringValue, exactMatch = false)).usesOperator(LikenessOperator.NOT_LIKE_IGNORE_CASE)
}
@Test(timeout = 500)
fun `isNull predicate uses IS_NULL operator`() {
assertThat(fieldInfo.isNull()).usesOperator(NullOperator.IS_NULL)
}
@Test(timeout = 500)
fun `notNull predicate uses NOT_NULL operator`() {
assertThat(fieldInfo.notNull()).usesOperator(NullOperator.NOT_NULL)
}
}

View File

@ -91,7 +91,6 @@
<ID>ComplexCondition:WireTransaction.kt$WireTransaction$notary != null &amp;&amp; (inputs.isNotEmpty() || references.isNotEmpty() || timeWindow != null)</ID>
<ID>ComplexMethod:AMQPBridgeManager.kt$AMQPBridgeManager.AMQPBridge$private fun clientArtemisMessageHandler(artemisMessage: ClientMessage)</ID>
<ID>ComplexMethod:AMQPBridgeTest.kt$AMQPBridgeTest$@Test(timeout=300_000) fun `test acked and nacked messages`()</ID>
<ID>ComplexMethod:AMQPChannelHandler.kt$AMQPChannelHandler$override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any)</ID>
<ID>ComplexMethod:AMQPTypeIdentifierParser.kt$AMQPTypeIdentifierParser$// Make sure our inputs aren't designed to blow things up. private fun validate(typeString: String)</ID>
<ID>ComplexMethod:ANSIProgressRenderer.kt$ANSIProgressRenderer$// Returns number of lines rendered. private fun renderLevel(ansi: Ansi, error: Boolean): Int</ID>
<ID>ComplexMethod:ANSIProgressRenderer.kt$ANSIProgressRenderer$@Synchronized protected fun draw(moveUp: Boolean, error: Throwable? = null)</ID>
@ -124,13 +123,10 @@
<ID>ComplexMethod:ConfigUtilities.kt$// For Iterables figure out the type parameter and apply the same logic as above on the individual elements. private fun Iterable&lt;*&gt;.toConfigIterable(field: Field): Iterable&lt;Any?&gt;</ID>
<ID>ComplexMethod:ConfigUtilities.kt$// TODO Move this to KeyStoreConfigHelpers. fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name, signingCertificateStore: FileBasedCertificateStoreSupplier, certificatesDirectory: Path, cryptoService: CryptoService? = null)</ID>
<ID>ComplexMethod:ConfigUtilities.kt$@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") // Reflect over the fields of the receiver and generate a value Map that can use to create Config object. private fun Any.toConfigMap(): Map&lt;String, Any&gt;</ID>
<ID>ComplexMethod:ConfigUtilities.kt$private fun Config.getCollectionValue(path: String, type: KType, onUnknownKeys: (Set&lt;String&gt;, logger: Logger) -&gt; Unit, nestedPath: String?, baseDirectory: Path?): Collection&lt;Any&gt;</ID>
<ID>ComplexMethod:ConfigUtilities.kt$private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set&lt;String&gt;, logger: Logger) -&gt; Unit, nestedPath: String?, baseDirectory: Path?): Any?</ID>
<ID>ComplexMethod:ConfigUtilities.kt$private fun convertValue(value: Any): Any</ID>
<ID>ComplexMethod:ConnectionStateMachine.kt$ConnectionStateMachine$override fun onConnectionFinal(event: Event)</ID>
<ID>ComplexMethod:ConnectionStateMachine.kt$ConnectionStateMachine$override fun onDelivery(event: Event)</ID>
<ID>ComplexMethod:ConstraintsUtils.kt$ fun AttachmentConstraint.canBeTransitionedFrom(input: AttachmentConstraint, attachment: ContractAttachment): Boolean</ID>
<ID>ComplexMethod:CordaCliWrapper.kt$fun CordaCliWrapper.start(args: Array&lt;String&gt;)</ID>
<ID>ComplexMethod:CordaPersistence.kt$CordaPersistence$private fun &lt;T&gt; inTopLevelTransaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int, recoverAnyNestedSQLException: Boolean, statement: DatabaseTransaction.() -&gt; T): T</ID>
<ID>ComplexMethod:CordaRPCClient.kt$CordaRPCClientConfiguration$override fun equals(other: Any?): Boolean</ID>
<ID>ComplexMethod:CordaRPCClientTest.kt$CordaRPCClientTest$@Test(timeout=300_000) fun `shutdown command stops the node`()</ID>
@ -182,7 +178,6 @@
<ID>ComplexMethod:RPCClientProxyHandler.kt$RPCClientProxyHandler$// This is the general function that transforms a client side RPC to internal Artemis messages. override fun invoke(proxy: Any, method: Method, arguments: Array&lt;out Any?&gt;?): Any?</ID>
<ID>ComplexMethod:RPCClientProxyHandler.kt$RPCClientProxyHandler$private fun attemptReconnect()</ID>
<ID>ComplexMethod:RPCServer.kt$RPCServer$private fun clientArtemisMessageHandler(artemisMessage: ClientMessage)</ID>
<ID>ComplexMethod:ReconnectingCordaRPCOps.kt$ReconnectingCordaRPCOps.ErrorInterceptingHandler$ private fun doInvoke(method: Method, args: Array&lt;out Any&gt;?, maxNumberOfAttempts: Int): Any?</ID>
<ID>ComplexMethod:ReconnectingCordaRPCOps.kt$ReconnectingCordaRPCOps.ReconnectingRPCConnection$ private tailrec fun establishConnectionWithRetry( retryInterval: Duration, roundRobinIndex: Int = 0, retries: Int = -1 ): CordaRPCConnection?</ID>
<ID>ComplexMethod:RemoteTypeCarpenter.kt$SchemaBuildingRemoteTypeCarpenter$override fun carpent(typeInformation: RemoteTypeInformation): Type</ID>
<ID>ComplexMethod:RpcReconnectTests.kt$RpcReconnectTests$ @Test(timeout=300_000) fun `test that the RPC client is able to reconnect and proceed after node failure, restart, or connection reset`()</ID>
@ -215,7 +210,6 @@
<ID>EmptyDefaultConstructor:FlowRetryTest.kt$AsyncRetryFlow$()</ID>
<ID>EmptyDefaultConstructor:FlowRetryTest.kt$RetryFlow$()</ID>
<ID>EmptyDefaultConstructor:FlowRetryTest.kt$ThrowingFlow$()</ID>
<ID>EmptyElseBlock:CordaCliWrapper.kt${ }</ID>
<ID>EmptyIfBlock:ContentSignerBuilder.kt$ContentSignerBuilder.SignatureOutputStream$if (alreadySigned) throw IllegalStateException("Cannot write to already signed object")</ID>
<ID>EmptyIfBlock:InMemoryIdentityService.kt$InMemoryIdentityService${ }</ID>
<ID>EmptyKtFile:KryoHook.kt$.KryoHook.kt</ID>
@ -331,7 +325,6 @@
<ID>ForbiddenComment:DigitalSignatureWithCert.kt$// TODO: Rename this to DigitalSignature.WithCert once we're happy for it to be public API. The methods will need documentation</ID>
<ID>ForbiddenComment:DriverDSLImpl.kt$DriverDSLImpl$// TODO: Derive name from the full picked name, don't just wrap the common name</ID>
<ID>ForbiddenComment:DriverDSLImpl.kt$DriverDSLImpl$//TODO: remove this once we can bundle quasar properly.</ID>
<ID>ForbiddenComment:DriverDSLImpl.kt$DriverDSLImpl.Companion$// TODO: There is pending work to fix this issue without custom blacklisting. See: https://r3-cev.atlassian.net/browse/CORDA-2164.</ID>
<ID>ForbiddenComment:DriverDSLImpl.kt$DriverDSLImpl.LocalNetworkMap$// TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/</ID>
<ID>ForbiddenComment:DummyFungibleContract.kt$DummyFungibleContract$// TODO: This doesn't work with the trader demo, so use the underlying key instead</ID>
<ID>ForbiddenComment:E2ETestKeyManagementService.kt$E2ETestKeyManagementService$// TODO: A full KeyManagementService implementation needs to record activity to the Audit Service and to limit</ID>
@ -388,6 +381,7 @@
<ID>ForbiddenComment:LegalNameValidator.kt$LegalNameValidator.Rule.Companion$// TODO: Implement confusable character detection if we add more scripts.</ID>
<ID>ForbiddenComment:LocalTypeInformationBuilder.kt$// TODO: Revisit this when Kotlin issue is fixed.</ID>
<ID>ForbiddenComment:LoggingBuyerFlow.kt$LoggingBuyerFlow$// TODO: This is potentially very expensive, and requires transaction details we may no longer have once</ID>
<ID>ForbiddenComment:LoopbackBridgeManager.kt$LoopbackBridgeManager.LoopbackBridge$// TODO: refactor MDC support, duplicated in AMQPBridgeManager.</ID>
<ID>ForbiddenComment:MockServices.kt$MockServices.Companion$// TODO: Can we use an X509 principal generator here?</ID>
<ID>ForbiddenComment:NetParams.kt$NetParamsSigner$// TODO: not supported</ID>
<ID>ForbiddenComment:NetworkBootstrapper.kt$NetworkBootstrapper$// TODO: pass a commandline parameter to the bootstrapper instead. Better yet, a notary config map</ID>
@ -630,8 +624,6 @@
<ID>FunctionNaming:VersionExtractorTest.kt$VersionExtractorTest$@Test(timeout=300_000) fun version_header_extraction_no_metadata()</ID>
<ID>FunctionNaming:VersionExtractorTest.kt$VersionExtractorTest$@Test(timeout=300_000) fun version_header_extraction_no_value()</ID>
<ID>FunctionNaming:VersionExtractorTest.kt$VersionExtractorTest$@Test(timeout=300_000) fun version_header_extraction_present()</ID>
<ID>FunctionNaming:VersionedParsingExampleTest.kt$VersionedParsingExampleTest$@Test(timeout=300_000) fun correct_parsing_function_is_used_for_present_version()</ID>
<ID>FunctionNaming:VersionedParsingExampleTest.kt$VersionedParsingExampleTest$@Test(timeout=300_000) fun default_value_is_used_for_absent_version()</ID>
<ID>LargeClass:AbstractNode.kt$AbstractNode&lt;S&gt; : SingletonSerializeAsToken</ID>
<ID>LargeClass:SingleThreadedStateMachineManager.kt$SingleThreadedStateMachineManager : StateMachineManagerStateMachineManagerInternal</ID>
<ID>LongMethod:FlowCookbook.kt$InitiatorFlow$@Suppress("RemoveExplicitTypeArguments") @Suspendable override fun call()</ID>
@ -655,7 +647,6 @@
<ID>LongParameterList:CertificateRevocationListNodeTests.kt$CertificateRevocationListNodeTests$(port: Int, name: CordaX500Name = ALICE_NAME, crlCheckSoftFail: Boolean, nodeCrlDistPoint: String = "http://${server.hostAndPort}/crl/node.crl", tlsCrlDistPoint: String? = "http://${server.hostAndPort}/crl/empty.crl", maxMessageSize: Int = MAX_MESSAGE_SIZE)</ID>
<ID>LongParameterList:CertificateRevocationListNodeTests.kt$CertificateRevocationListNodeTests.Companion$(clrServer: CrlServer, signatureAlgorithm: String, caCertificate: X509Certificate, caPrivateKey: PrivateKey, endpoint: String, indirect: Boolean, vararg serialNumbers: BigInteger)</ID>
<ID>LongParameterList:CertificateStoreStubs.kt$CertificateStoreStubs.P2P.Companion$(baseDirectory: Path, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, keyStoreFileName: String = KeyStore.DEFAULT_STORE_FILE_NAME, keyStorePassword: String = KeyStore.DEFAULT_STORE_PASSWORD, keyPassword: String = keyStorePassword, trustStoreFileName: String = TrustStore.DEFAULT_STORE_FILE_NAME, trustStorePassword: String = TrustStore.DEFAULT_STORE_PASSWORD)</ID>
<ID>LongParameterList:CertificateStoreStubs.kt$CertificateStoreStubs.P2P.Companion$(certificatesDirectory: Path, keyStoreFileName: String = KeyStore.DEFAULT_STORE_FILE_NAME, keyStorePassword: String = KeyStore.DEFAULT_STORE_PASSWORD, keyPassword: String = keyStorePassword, trustStoreFileName: String = TrustStore.DEFAULT_STORE_FILE_NAME, trustStorePassword: String = TrustStore.DEFAULT_STORE_PASSWORD, trustStoreKeyPassword: String = TrustStore.DEFAULT_KEY_PASSWORD, @Suppress("UNUSED_PARAMETER") useOpenSsl: Boolean = false)</ID>
<ID>LongParameterList:ContractAttachment.kt$ContractAttachment.Companion$(attachment: Attachment, contract: ContractClassName, additionalContracts: Set&lt;ContractClassName&gt; = emptySet(), uploader: String? = null, signerKeys: List&lt;PublicKey&gt; = emptyList(), version: Int = DEFAULT_CORDAPP_VERSION)</ID>
<ID>LongParameterList:ContractFunctions.kt$(expiry: String, notional: BigDecimal, strike: BigDecimal, foreignCurrency: Currency, domesticCurrency: Currency, partyA: Party, partyB: Party)</ID>
<ID>LongParameterList:ContractFunctions.kt$(expiry: String, notional: Long, strike: Double, foreignCurrency: Currency, domesticCurrency: Currency, partyA: Party, partyB: Party)</ID>
@ -925,7 +916,6 @@
<ID>MagicNumber:IrsDemoWebApplication.kt$IrsDemoWebApplication$1000</ID>
<ID>MagicNumber:JarScanningCordappLoader.kt$CordappLoaderTemplate$36</ID>
<ID>MagicNumber:JarScanningCordappLoader.kt$CordappLoaderTemplate$64</ID>
<ID>MagicNumber:JarScanningCordappLoader.kt$JarScanningCordappLoader$1000</ID>
<ID>MagicNumber:JarSignatureCollector.kt$JarSignatureCollector$1024</ID>
<ID>MagicNumber:JarSignatureTestUtils.kt$JarSignatureTestUtils$14</ID>
<ID>MagicNumber:KMSUtils.kt$3650</ID>
@ -1229,8 +1219,6 @@
<ID>MatchingDeclarationName:Query.kt$net.corda.webserver.api.Query.kt</ID>
<ID>MatchingDeclarationName:ReceiveAllFlowTests.kt$net.corda.coretests.flows.ReceiveAllFlowTests.kt</ID>
<ID>MatchingDeclarationName:ReferenceInputStateTests.kt$net.corda.coretests.transactions.ReferenceInputStateTests.kt</ID>
<ID>MatchingDeclarationName:RigorousMock.kt$net.corda.testing.internal.RigorousMock.kt</ID>
<ID>MatchingDeclarationName:RpcServerCordaFutureSerialiser.kt$net.corda.node.serialization.amqp.RpcServerCordaFutureSerialiser.kt</ID>
<ID>MatchingDeclarationName:SSLHelper.kt$net.corda.nodeapi.internal.protonwrapper.netty.SSLHelper.kt</ID>
<ID>MatchingDeclarationName:SampleData.kt$net.corda.deterministic.verifier.SampleData.kt</ID>
<ID>MatchingDeclarationName:SerializationHelper.kt$net.corda.networkbuilder.serialization.SerializationHelper.kt</ID>
@ -1285,7 +1273,6 @@
<ID>NestedBlockDepth:RPCClientProxyHandler.kt$RPCClientProxyHandler$// The handler for Artemis messages. private fun artemisMessageHandler(message: ClientMessage)</ID>
<ID>NestedBlockDepth:ShutdownManager.kt$ShutdownManager$fun shutdown()</ID>
<ID>NestedBlockDepth:SpringDriver.kt$SpringBootDriverDSL$private fun queryWebserver(handle: NodeHandle, process: Process, checkUrl: String): WebserverHandle</ID>
<ID>NestedBlockDepth:StartedFlowTransition.kt$StartedFlowTransition$private fun TransitionBuilder.sendToSessionsTransition(sourceSessionIdToMessage: Map&lt;SessionId, SerializedBytes&lt;Any&gt;&gt;)</ID>
<ID>NestedBlockDepth:StatusTransitions.kt$StatusTransitions$ fun verify(tx: LedgerTransaction)</ID>
<ID>NestedBlockDepth:ThrowableSerializer.kt$ThrowableSerializer$override fun fromProxy(proxy: ThrowableProxy): Throwable</ID>
<ID>NestedBlockDepth:TransactionVerifierServiceInternal.kt$Verifier$ private fun verifyConstraintsValidity(contractAttachmentsByContract: Map&lt;ContractClassName, ContractAttachment&gt;)</ID>
@ -1316,7 +1303,6 @@
<ID>SpreadOperator:ConfigUtilities.kt$(*pairs)</ID>
<ID>SpreadOperator:Configuration.kt$Configuration.Validation.Error$(*(containingPath.toList() + this.containingPath).toTypedArray())</ID>
<ID>SpreadOperator:ContractJarTestUtils.kt$ContractJarTestUtils$(jarName, *contractNames.map{ "${it.replace(".", "/")}.class" }.toTypedArray())</ID>
<ID>SpreadOperator:CordaCliWrapper.kt$(RunLast().useOut(System.out).useAnsi(defaultAnsiMode), DefaultExceptionHandler&lt;List&lt;Any&gt;&gt;().useErr(System.err).useAnsi(defaultAnsiMode).andExit(ExitCodes.FAILURE), *args)</ID>
<ID>SpreadOperator:CordaRPCOpsImpl.kt$CordaRPCOpsImpl$(logicType, context(), *args)</ID>
<ID>SpreadOperator:CordaX500Name.kt$CordaX500Name.Companion$(*Locale.getISOCountries(), unspecifiedCountry)</ID>
<ID>SpreadOperator:CustomCordapp.kt$CustomCordapp$(*classes.map { it.name }.toTypedArray())</ID>
@ -1326,7 +1312,6 @@
<ID>SpreadOperator:DockerInstantiator.kt$DockerInstantiator$(*it.toTypedArray())</ID>
<ID>SpreadOperator:DummyContract.kt$DummyContract.Companion$( /* INPUTS */ *priors.toTypedArray(), /* COMMAND */ Command(cmd, priorState.owner.owningKey), /* OUTPUT */ StateAndContract(state, PROGRAM_ID) )</ID>
<ID>SpreadOperator:DummyContract.kt$DummyContract.Companion$(*items)</ID>
<ID>SpreadOperator:DummyContractV2.kt$DummyContractV2.Companion$( /* INPUTS */ *priors.toTypedArray(), /* COMMAND */ Command(cmd, priorState.owners.map { it.owningKey }), /* OUTPUT */ StateAndContract(state, DummyContractV2.PROGRAM_ID) )</ID>
<ID>SpreadOperator:ExceptionsErrorCodeFunctions.kt$(*fields)</ID>
<ID>SpreadOperator:ExceptionsErrorCodeFunctions.kt$(*fields, cause.staticLocationBasedHash(hashedFields, visited + cause))</ID>
<ID>SpreadOperator:ExceptionsErrorCodeFunctions.kt$(*hashedFields.invoke(this))</ID>
@ -1491,7 +1476,6 @@
<ID>TooGenericExceptionCaught:BankOfCordaWebApi.kt$BankOfCordaWebApi$e: Exception</ID>
<ID>TooGenericExceptionCaught:BlobInspector.kt$BlobInspector$e: Exception</ID>
<ID>TooGenericExceptionCaught:BootstrapperView.kt$BootstrapperView$e: Exception</ID>
<ID>TooGenericExceptionCaught:BridgeControlListener.kt$BridgeControlListener$ex: Exception</ID>
<ID>TooGenericExceptionCaught:BrokerJaasLoginModule.kt$BrokerJaasLoginModule$e: Exception</ID>
<ID>TooGenericExceptionCaught:CertRole.kt$CertRole.Companion$ex: ArrayIndexOutOfBoundsException</ID>
<ID>TooGenericExceptionCaught:CheckpointAgent.kt$CheckpointAgent.Companion$e: Exception</ID>
@ -1506,9 +1490,6 @@
<ID>TooGenericExceptionCaught:ContractUpgradeTransactions.kt$ContractUpgradeWireTransaction$e: Exception</ID>
<ID>TooGenericExceptionCaught:CordaAuthenticationPlugin.kt$CordaAuthenticationPlugin$e: Exception</ID>
<ID>TooGenericExceptionCaught:CordaClassResolver.kt$LoggingWhitelist.Companion$ioEx: Exception</ID>
<ID>TooGenericExceptionCaught:CordaFutureImpl.kt$CordaFutureImpl$e: Exception</ID>
<ID>TooGenericExceptionCaught:CordaFutureImpl.kt$ValueOrException$e: Exception</ID>
<ID>TooGenericExceptionCaught:CordaFutureImpl.kt$e: Exception</ID>
<ID>TooGenericExceptionCaught:CordaPersistence.kt$CordaPersistence$e: Exception</ID>
<ID>TooGenericExceptionCaught:CordaRPCClientTest.kt$CordaRPCClientTest$e: Exception</ID>
<ID>TooGenericExceptionCaught:CordaRPCOpsImpl.kt$CordaRPCOpsImpl$e: Exception</ID>
@ -1644,7 +1625,6 @@
<ID>TooGenericExceptionCaught:ValidatingNotaryFlow.kt$ValidatingNotaryFlow$e: Exception</ID>
<ID>TooGenericExceptionCaught:VaultStateMigration.kt$VaultStateIterator$e: Exception</ID>
<ID>TooGenericExceptionCaught:VaultStateMigration.kt$VaultStateMigration$e: Exception</ID>
<ID>TooGenericExceptionCaught:VersionedParsingExampleTest.kt$VersionedParsingExampleTest.RpcSettingsSpec$e: Exception</ID>
<ID>TooGenericExceptionCaught:WebServer.kt$WebServer$e: Exception</ID>
<ID>TooGenericExceptionCaught:WebServer.kt$e: Exception</ID>
<ID>TooGenericExceptionCaught:WebServer.kt$ex: Exception</ID>
@ -1699,7 +1679,6 @@
<ID>TooManyFunctions:CryptoUtils.kt$net.corda.core.crypto.CryptoUtils.kt</ID>
<ID>TooManyFunctions:Currencies.kt$net.corda.finance.Currencies.kt</ID>
<ID>TooManyFunctions:Driver.kt$DriverParameters</ID>
<ID>TooManyFunctions:DriverDSLImpl.kt$DriverDSLImpl : InternalDriverDSL</ID>
<ID>TooManyFunctions:EncodingUtils.kt$net.corda.core.utilities.EncodingUtils.kt</ID>
<ID>TooManyFunctions:FlowLogic.kt$FlowLogic&lt;out T&gt;</ID>
<ID>TooManyFunctions:FlowStateMachineImpl.kt$FlowStateMachineImpl&lt;R&gt; : FiberFlowStateMachineFlowFiber</ID>
@ -1885,8 +1864,6 @@
<ID>VariableNaming:VaultQueryTests.kt$VaultQueryTestsBase$// Beware: do not use `MyContractClass::class.qualifiedName` as this returns a fully qualified name using "dot" notation for enclosed class val MYCONTRACT_ID = "net.corda.node.services.vault.VaultQueryTestsBase\$MyContractClass"</ID>
<ID>VariableNaming:ZeroCouponBond.kt$ZeroCouponBond$val TEST_TX_TIME_1: Instant get() = Instant.parse("2017-09-02T12:00:00.00Z")</ID>
<ID>WildcardImport:AMQPClient.kt$import io.netty.channel.*</ID>
<ID>WildcardImport:AMQPClientSerializationScheme.kt$import net.corda.serialization.internal.*</ID>
<ID>WildcardImport:AMQPClientSerializationScheme.kt$import net.corda.serialization.internal.amqp.*</ID>
<ID>WildcardImport:AMQPRemoteTypeModel.kt$import net.corda.serialization.internal.model.*</ID>
<ID>WildcardImport:AMQPSerializationScheme.kt$import net.corda.core.serialization.*</ID>
<ID>WildcardImport:AMQPServerSerializationScheme.kt$import net.corda.serialization.internal.amqp.*</ID>
@ -2023,9 +2000,6 @@
<ID>WildcardImport:CordaModule.kt$import net.corda.core.crypto.*</ID>
<ID>WildcardImport:CordaModule.kt$import net.corda.core.identity.*</ID>
<ID>WildcardImport:CordaModule.kt$import net.corda.core.transactions.*</ID>
<ID>WildcardImport:CordaRPCClientTest.kt$import net.corda.core.context.*</ID>
<ID>WildcardImport:CordaRPCClientTest.kt$import net.corda.core.messaging.*</ID>
<ID>WildcardImport:CordaRPCClientTest.kt$import net.corda.testing.core.*</ID>
<ID>WildcardImport:CordaRPCOps.kt$import net.corda.core.node.services.vault.*</ID>
<ID>WildcardImport:CordaRPCOpsImplTest.kt$import net.corda.core.messaging.*</ID>
<ID>WildcardImport:CordaRPCOpsImplTest.kt$import org.assertj.core.api.Assertions.*</ID>
@ -2121,8 +2095,6 @@
<ID>WildcardImport:FlowStateMachineImpl.kt$import net.corda.core.flows.*</ID>
<ID>WildcardImport:FlowStateMachineImpl.kt$import net.corda.core.internal.*</ID>
<ID>WildcardImport:FlowsDrainingModeContentionTest.kt$import net.corda.core.flows.*</ID>
<ID>WildcardImport:FxTransactionBuildTutorial.kt$import net.corda.core.contracts.*</ID>
<ID>WildcardImport:FxTransactionBuildTutorial.kt$import net.corda.core.flows.*</ID>
<ID>WildcardImport:FxTransactionBuildTutorialTest.kt$import net.corda.finance.*</ID>
<ID>WildcardImport:GenericsTests.kt$import net.corda.serialization.internal.amqp.testutils.*</ID>
<ID>WildcardImport:Gui.kt$import tornadofx.*</ID>
@ -2168,10 +2140,7 @@
<ID>WildcardImport:InternalMockNetwork.kt$import net.corda.core.internal.*</ID>
<ID>WildcardImport:InternalMockNetwork.kt$import net.corda.node.services.config.*</ID>
<ID>WildcardImport:InternalMockNetwork.kt$import net.corda.testing.node.*</ID>
<ID>WildcardImport:InternalSerializationTestHelpers.kt$import net.corda.serialization.internal.*</ID>
<ID>WildcardImport:InternalTestUtils.kt$import net.corda.core.contracts.*</ID>
<ID>WildcardImport:InternalUtils.kt$import java.security.cert.*</ID>
<ID>WildcardImport:InternalUtils.kt$import net.corda.core.crypto.*</ID>
<ID>WildcardImport:IssuerModel.kt$import tornadofx.*</ID>
<ID>WildcardImport:JVMConfig.kt$import tornadofx.*</ID>
<ID>WildcardImport:JacksonSupport.kt$import com.fasterxml.jackson.core.*</ID>
@ -2249,7 +2218,6 @@
<ID>WildcardImport:NetworkBootstrapper.kt$import net.corda.nodeapi.internal.*</ID>
<ID>WildcardImport:NetworkBootstrapperRunnerTests.kt$import org.junit.*</ID>
<ID>WildcardImport:NetworkBootstrapperTest.kt$import net.corda.core.internal.*</ID>
<ID>WildcardImport:NetworkBootstrapperTest.kt$import net.corda.testing.core.*</ID>
<ID>WildcardImport:NetworkBuilder.kt$import net.corda.networkbuilder.nodes.*</ID>
<ID>WildcardImport:NetworkIdentityModel.kt$import net.corda.client.jfx.utils.*</ID>
<ID>WildcardImport:NetworkMapServer.kt$import javax.ws.rs.*</ID>
@ -2362,7 +2330,6 @@
<ID>WildcardImport:PathUtils.kt$import java.io.*</ID>
<ID>WildcardImport:PathUtils.kt$import java.nio.file.*</ID>
<ID>WildcardImport:PersistentIdentityMigrationNewTableTest.kt$import net.corda.testing.core.*</ID>
<ID>WildcardImport:PersistentIdentityServiceTests.kt$import net.corda.testing.core.*</ID>
<ID>WildcardImport:PersistentNetworkMapCacheTest.kt$import net.corda.testing.core.*</ID>
<ID>WildcardImport:PersistentStateServiceTests.kt$import net.corda.core.contracts.*</ID>
<ID>WildcardImport:Portfolio.kt$import net.corda.core.contracts.*</ID>
@ -2387,8 +2354,6 @@
<ID>WildcardImport:QueryCriteriaUtils.kt$import net.corda.core.node.services.vault.LikenessOperator.*</ID>
<ID>WildcardImport:RPCMultipleInterfacesTests.kt$import org.junit.Assert.*</ID>
<ID>WildcardImport:RPCSecurityManagerImpl.kt$import org.apache.shiro.authc.*</ID>
<ID>WildcardImport:RPCServer.kt$import net.corda.core.utilities.*</ID>
<ID>WildcardImport:RPCServer.kt$import org.apache.activemq.artemis.api.core.client.*</ID>
<ID>WildcardImport:ReceiveFinalityFlowTest.kt$import net.corda.node.services.statemachine.StaffedFlowHospital.*</ID>
<ID>WildcardImport:ReceiveFinalityFlowTest.kt$import net.corda.testing.node.internal.*</ID>
<ID>WildcardImport:ReceiveTransactionFlow.kt$import net.corda.core.contracts.*</ID>
@ -2427,7 +2392,6 @@
<ID>WildcardImport:SearchField.kt$import tornadofx.*</ID>
<ID>WildcardImport:SecureHashTest.kt$import org.junit.Assert.*</ID>
<ID>WildcardImport:SendTransactionFlow.kt$import net.corda.core.internal.*</ID>
<ID>WildcardImport:SerializationEnvironmentRule.kt$import net.corda.testing.internal.*</ID>
<ID>WildcardImport:SerializationHelper.kt$import java.lang.reflect.*</ID>
<ID>WildcardImport:SerializationHelper.kt$import net.corda.core.serialization.*</ID>
<ID>WildcardImport:SerializationOutputTests.kt$import java.time.*</ID>
@ -2572,7 +2536,6 @@
<ID>WildcardImport:VaultWithCashTest.kt$import net.corda.testing.core.*</ID>
<ID>WildcardImport:VaultWithCashTest.kt$import net.corda.testing.internal.vault.*</ID>
<ID>WildcardImport:VerifyTransactionTest.kt$import net.corda.finance.contracts.asset.Cash.Commands.*</ID>
<ID>WildcardImport:VersionedParsingExampleTest.kt$import net.corda.common.configuration.parsing.internal.*</ID>
<ID>WildcardImport:WebServerController.kt$import tornadofx.*</ID>
<ID>WildcardImport:WhitelistBasedTypeModelConfiguration.kt$import org.apache.qpid.proton.amqp.*</ID>
<ID>WildcardImport:WhitelistGenerator.kt$import net.corda.core.internal.*</ID>
@ -2581,8 +2544,6 @@
<ID>WildcardImport:WireTransaction.kt$import net.corda.core.internal.*</ID>
<ID>WildcardImport:WithFinality.kt$import net.corda.core.flows.*</ID>
<ID>WildcardImport:WithMockNet.kt$import com.natpryce.hamkrest.*</ID>
<ID>WildcardImport:WorkflowTransactionBuildTutorial.kt$import net.corda.core.contracts.*</ID>
<ID>WildcardImport:WorkflowTransactionBuildTutorial.kt$import net.corda.core.flows.*</ID>
<ID>WildcardImport:X509CRLSerializer.kt$import net.corda.serialization.internal.amqp.*</ID>
<ID>WildcardImport:X509CertificateSerializer.kt$import net.corda.serialization.internal.amqp.*</ID>
<ID>WildcardImport:X509EdDSAEngine.kt$import java.security.*</ID>

View File

@ -272,7 +272,7 @@ In addition ``FlowLogic`` provides functions that can receive messages from mult
* Receives from all ``FlowSession`` objects specified in the passed in list. The received types must be the same.
* ``sendAll(payload: Any, sessions: Set<FlowSession>)``
* Sends the ``payload`` object to all the provided ``FlowSession``\s.
* ``sendAll(payloadsPerSession: Map<FlowSession, Any>)``
* ``sendAllMap(payloadsPerSession: Map<FlowSession, Any>)``
* Sends a potentially different payload to each ``FlowSession``, as specified by the provided ``payloadsPerSession``.
.. note:: It's more efficient to call ``sendAndReceive`` instead of calling ``send`` and then ``receive``. It's also more efficient to call ``sendAll``/``receiveAll`` instead of multiple ``send``/``receive`` respectively.

View File

@ -111,6 +111,8 @@ Unreleased
* Added new node configuration option to exclude packages from Quasar instrumentation.
* Fixed the operator used by the ``notEqual`` predicate
.. _changelog_v4.1:
Version 4.1

View File

@ -11,8 +11,7 @@ modern business transactions. It is unique in its aim to build a platform for b
retaining strict privacy. Corda provides an implementation of this vision in a code base which others are free to build on, contribute to
or innovate around. The mission of Corda is further detailed in the `Corda introductory white paper`_.
The project is supported and maintained by the `R3 Alliance <https://www.r3.com>`_, or R3 for short, which consists of over two hundred firms
working together to build and maintain this open source enterprise-grade blockchain platform.
The project is supported and maintained by `R3 <https://www.r3.com>`_.
Community Locations
-------------------
@ -44,12 +43,12 @@ Community maintainers
^^^^^^^^^^^^^^^^^^^^^
Current community maintainers:
* `Joel Dudley <https://github.com/joeldudleyr3>`_ - Contact me:
* `Rick Parker <https://github.com/rick-r3>`_ - Contact me:
* On the `Corda Slack team <http://slack.corda.net/>`_, either in the ``#community`` channel or by direct message using the handle
``@joel``
``@parkri``
* By email: joel.dudley at r3.com
* By email: rick.parker at r3.com
We anticipate additional maintainers joining the project in the future from across the community.
@ -60,13 +59,12 @@ Over two hundred individuals have contributed to the development of Corda. You c
Transparency and Conflict Policy
--------------------------------
The project is supported and maintained by the `R3 Alliance <https://www.r3.com>`_, which consists of over two hundred firms working together
to build and maintain this open source enterprise-grade blockchain platform. We develop in the open and publish our
The project is supported and maintained by `R3 <https://www.r3.com>`_. We develop in the open and publish our
`Jira <https://r3-cev.atlassian.net/projects/CORDA/summary>`_ to give everyone visibility. R3 also maintains and distributes a commercial
distribution of Corda. Our vision is that distributions of Corda be compatible and interoperable, and our contribution and code review
guidelines are designed in part to enable this.
As the R3 Alliance is maintainer of the project and also develops a commercial distribution of Corda, what happens if a member of the
As R3 is maintainer of the project and also develops a commercial distribution of Corda, what happens if a member of the
community contributes a feature which the R3 team have implemented only in their commercial product? How is this apparent conflict managed?
Our approach is simple: if the contribution meets the standards for the project (see above), then the existence of a competing commercial
implementation will not be used as a reason to reject it. In other words, it is our policy that should a community feature be contributed

View File

@ -16,14 +16,6 @@ So, what does it mean for a piece of code to be fully deterministic? Ultimately
as a function, is pure. In other words, given the same set of inputs, it will always produce the same set of outputs
without inflicting any side-effects that might later affect the computation.
.. important:: The code in the DJVM module has not yet been integrated with the rest of the platform. It will eventually become a
part of the node and enforce deterministic and secure execution of smart contract code, which is mobile and may
propagate around the network without human intervention.
Currently, it stands alone as an evaluation version. We want to give developers the ability to start trying it out and
get used to developing deterministic code under the set of constraints that we envision will be placed on contract code
in the future.
Non-Determinism
~~~~~~~~~~~~~~~
@ -272,34 +264,73 @@ The DJVM doesn't support multi-threading and so synchronised methods and code bl
use in sandboxed code. Consequently, we automatically transform them into ordinary methods and code blocks instead.
Future Work
~~~~~~~~~~~
Trying out the DJVM
~~~~~~~~~~~~~~~~~~~
Further work is planned:
.. warning:: The code in the DJVM module is still a beta release. It has been partially integrated with Corda to allow contract
verification. However, DJVM-enabled nodes cannot yet participate in a general Corda network containing nodes that do not use the DJVM. It
is provided to allow developers to try out the DJVM and experiment with developing deterministic code under the set of constraints that
we envision will be placed on contract code in the future.
* To enable controlled use of reflection APIs.
Tweaking Your Contract Code
...........................
* Currently, dynamic invocation is disallowed. Allow specific lambda and
string concatenation meta-factories used by Java code itself.
CorDapp developers may need to tweak their contract CorDapps for use inside the DJVM. This is because not every class, constructor or
method defined in the ``corda-core`` and ``corda-serialization`` modules is available when running inside the sandbox.
* Map more mathematical operations to use their 'exact' counterparts.
During development, you can choose to compile individual CorDapp modules against the DJVM by defining the following
``deterministic.gradle`` script plugin:
* General tightening of the enforced constraints.
.. code-block:: shell
* Cost accounting of runtime metrics such as memory allocation, branching and
exception handling. More specifically defining sensible runtime thresholds
and make further improvements to the instrumentation.
configurations {
compileClasspath { Configuration c -> deterministic(c) }
}
* More sophisticated runtime accounting as discussed in `Runtime Costing`_.
private final void deterministic(Configuration configuration) {
if (configuration.state == Configuration.State.UNRESOLVED) {
// Ensure that this module uses the deterministic Corda artifacts.
configuration.resolutionStrategy.dependencySubstitution {
substitute module("$corda_release_group:corda-serialization") with module("$corda_release_group:corda-serialization-deterministic:$corda_release_version")
substitute module("$corda_release_group:corda-core") with module("$corda_release_group:corda-core-deterministic:$corda_release_version")
}
}
}
And applying it to individual modules of your CorDapp using:
Command-line Tool
~~~~~~~~~~~~~~~~~
.. code-block:: shell
apply from: "${rootProject.projectDir}/deterministic.gradle"
Uses of Corda's core or serialization APIs that are unavailable inside the sandbox will then cause compilation errors.
Note however that successful compilation against ``corda-core-deterministic`` and ``corda-serialization-deterministic`` is
not sufficient. The only way to be sure that a piece of code is deterministic is to actually run it inside a DJVM sandbox,
as described below.
Enabling Use of the DJVM for a Node
...................................
You can enable the DJVM for your node by adding the following line to your node's ``node.conf`` file:
.. code-block:: shell
systemProperties = { "net.corda.djvm" = true }
This will cause your node to sandbox every call to ``Contract.verify``. If your transaction contains a source of non-determinism,
transaction verification will fail.
Alternatively, you can enable the DJVM when creating nodes via DemoBench by ticking the ``Deterministic Contract Verification`` checkbox
when creating the initial notary node.
Using the Command-line Tool
...........................
You can download and unpack ``corda-djvm-cli.zip`` from the R3 Artifactory.
Alternatively, you can build it yourself from the source as follows.
Open your terminial and clone the DJVM repository from GitHub:
Open your terminal and clone the DJVM repository from GitHub:
.. code-block:: shell

View File

@ -51,6 +51,26 @@ Security enhancements
* The ability to SSH into the standalone shell has been removed
* A new read-only RPC user role template has been documented in :doc:`shell`
Changes to integration testing
+++++++++++++++++++++++
The "out-of-process" nodes spawned through Driver DSL (see :doc:`tutorial-integration-testing`) will no longer accidentally contain your CorDapps on their application classpath. The list of items that will be automatically filtered out include:
* Directories (only regular files are allowed)
* Jars with Maven classifiers ``tests`` or ``test``
* Jars with any Cordapp attributes in their manifests (any of those listed in :doc:`cordapp-build-systems` or ``Target-Platform-Version`` and ``Min-Platform-Version`` if both are present)
* Jars with the ``Corda-Testing`` attribute in their manifests. The manifest of the following artifacts has been updated to include the ``Corda-Testing`` attribute:
* ``corda-node-driver``
* ``corda-test-utils``
* ``corda-test-common``
* ``corda-test-db``
* ``corda-mock``
* Files whose names start with ``corda-mock``, ``junit``, ``testng`` or ``mockito``
Some of your existing integration tests might implicitly be relying on the presence of the above files, so please keep this in mind when upgrading your version of Corda.
Platform version change
~~~~~~~~~~~~~~~~~~~~~~~
@ -59,6 +79,10 @@ Given the addition of new APIs, the platform version of Corda 4.4 has been bumpe
For more information on platform version, please see :doc:`versioning`. For more details on upgrading a CorDapp to use platform version 5, please see :doc:`app-upgrade-notes`.
Known Issues
~~~~~~~~~~~~
Changes introduced in Corda 4.4 to increase ledger integrity have highlighted limitations regarding database transactions. To prevent flows from continuing to process after a database transaction has failed to commit or suffered from a pre-commit persistence exception, extra database flushes have been added. These extra flushes can cause exceptions to be thrown where they were not before (or cause different exception types to be raised compared to Corda 4.3 or previous versions). In general, CorDapp developers should not expect to be able to catch exceptions thrown during a database transaction and then continue with further DB operations as part of the same flow. A safer pattern involves allowing the flow to fail and be retried
Issues Fixed
~~~~~~~~~~~~

View File

@ -1,5 +1,5 @@
#Wed Aug 21 10:48:19 BST 2019
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
distributionUrl=https\://gradleproxy:gradleproxy@software.r3.com/artifactory/gradle-proxy/gradle-5.4.1-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStorePath=wrapper/dists

View File

@ -19,6 +19,8 @@ dependencies {
compile "org.apache.activemq:artemis-core-client:${artemis_version}"
compile "org.apache.activemq:artemis-commons:${artemis_version}"
compile "io.netty:netty-handler-proxy:$netty_version"
// TypeSafe Config: for simple and human friendly config files.
compile "com.typesafe:config:$typesafe_config_version"

View File

@ -4,6 +4,9 @@ import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_USER
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pConnectorTcpTransport
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pConnectorTcpTransportFromList
import net.corda.nodeapi.internal.config.MessagingServerConnectionConfiguration
import net.corda.nodeapi.internal.config.MutualSslConfiguration
import org.apache.activemq.artemis.api.core.client.*
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
@ -17,28 +20,55 @@ interface ArtemisSessionProvider {
class ArtemisMessagingClient(private val config: MutualSslConfiguration,
private val serverAddress: NetworkHostAndPort,
private val maxMessageSize: Int,
private val failoverCallback: ((FailoverEventType) -> Unit)? = null) : ArtemisSessionProvider {
private val autoCommitSends: Boolean = true,
private val autoCommitAcks: Boolean = true,
private val confirmationWindowSize: Int = -1,
private val messagingServerConnectionConfig: MessagingServerConnectionConfiguration? = null,
private val backupServerAddressPool: List<NetworkHostAndPort> = emptyList(),
private val failoverCallback: ((FailoverEventType) -> Unit)? = null
) : ArtemisSessionProvider {
companion object {
private val log = loggerFor<ArtemisMessagingClient>()
const val CORDA_ARTEMIS_CALL_TIMEOUT_PROP_NAME = "net.corda.nodeapi.artemismessagingclient.CallTimeout"
const val CORDA_ARTEMIS_CALL_TIMEOUT_DEFAULT = 5000L
}
class Started(val sessionFactory: ClientSessionFactory, val session: ClientSession, val producer: ClientProducer)
class Started(val serverLocator: ServerLocator, val sessionFactory: ClientSessionFactory, val session: ClientSession, val producer: ClientProducer)
override var started: Started? = null
private set
override fun start(): Started = synchronized(this) {
check(started == null) { "start can't be called twice" }
val tcpTransport = p2pConnectorTcpTransport(serverAddress, config)
val backupTransports = p2pConnectorTcpTransportFromList(backupServerAddressPool, config)
log.info("Connecting to message broker: $serverAddress")
// TODO Add broker CN to config for host verification in case the embedded broker isn't used
val tcpTransport = ArtemisTcpTransport.p2pConnectorTcpTransport(serverAddress, config)
val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply {
if (backupTransports.isNotEmpty()) {
log.info("Back-up message broker addresses: $backupServerAddressPool")
}
// If back-up artemis addresses are configured, the locator will be created using HA mode.
@Suppress("SpreadOperator")
val locator = ActiveMQClient.createServerLocator(backupTransports.isNotEmpty(), *(listOf(tcpTransport) + backupTransports).toTypedArray()).apply {
// Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this
// would be the default and the two lines below can be deleted.
connectionTTL = 60000
clientFailureCheckPeriod = 30000
callFailoverTimeout = java.lang.Long.getLong(CORDA_ARTEMIS_CALL_TIMEOUT_PROP_NAME, CORDA_ARTEMIS_CALL_TIMEOUT_DEFAULT)
callTimeout = java.lang.Long.getLong(CORDA_ARTEMIS_CALL_TIMEOUT_PROP_NAME, CORDA_ARTEMIS_CALL_TIMEOUT_DEFAULT)
minLargeMessageSize = maxMessageSize
isUseGlobalPools = nodeSerializationEnv != null
confirmationWindowSize = this@ArtemisMessagingClient.confirmationWindowSize
producerWindowSize = -1
messagingServerConnectionConfig?.let {
connectionLoadBalancingPolicyClassName = RoundRobinConnectionPolicy::class.java.canonicalName
reconnectAttempts = messagingServerConnectionConfig.reconnectAttempts(isHA)
retryInterval = messagingServerConnectionConfig.retryInterval().toMillis()
retryIntervalMultiplier = messagingServerConnectionConfig.retryIntervalMultiplier()
maxRetryInterval = messagingServerConnectionConfig.maxRetryInterval(isHA).toMillis()
isFailoverOnInitialConnection = messagingServerConnectionConfig.failoverOnInitialAttempt(isHA)
initialConnectAttempts = messagingServerConnectionConfig.initialConnectAttempts(isHA)
}
addIncomingInterceptor(ArtemisMessageSizeChecksInterceptor(maxMessageSize))
}
val sessionFactory = locator.createSessionFactory()
@ -50,23 +80,24 @@ class ArtemisMessagingClient(private val config: MutualSslConfiguration,
// using our TLS certificate.
// Note that the acknowledgement of messages is not flushed to the Artermis journal until the default buffer
// size of 1MB is acknowledged.
val session = sessionFactory!!.createSession(NODE_P2P_USER, NODE_P2P_USER, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
val session = sessionFactory!!.createSession(NODE_P2P_USER, NODE_P2P_USER, false, autoCommitSends, autoCommitAcks, false, DEFAULT_ACK_BATCH_SIZE)
session.start()
// Create a general purpose producer.
val producer = session.createProducer()
return Started(sessionFactory, session, producer).also { started = it }
return Started(locator, sessionFactory, session, producer).also { started = it }
}
override fun stop() = synchronized(this) {
started?.run {
producer.close()
// Since we are leaking the session outside of this class it may well be already closed.
if(!session.isClosed) {
if (session.stillOpen()) {
// Ensure any trailing messages are committed to the journal
session.commit()
}
// Closing the factory closes all the sessions it produced as well.
sessionFactory.close()
serverLocator.close()
}
started = null
}

View File

@ -34,6 +34,7 @@ class ArtemisMessagingComponent {
// This is a rough guess on the extra space needed on top of maxMessageSize to store the journal.
// TODO: we might want to make this value configurable.
const val JOURNAL_HEADER_SIZE = 1024
object P2PMessagingHeaders {
// This is a "property" attached to an Artemis MQ message object, which contains our own notion of "topic".
// We should probably try to unify our notion of "topic" (really, just a string that identifies an endpoint
@ -123,6 +124,11 @@ class ArtemisMessagingComponent {
require(address.startsWith(PEERS_PREFIX)) { "Failed to map address: $address to a remote topic as it is not in the $PEERS_PREFIX namespace" }
return P2P_PREFIX + address.substring(PEERS_PREFIX.length)
}
fun translateInboxAddressToLocalQueue(address: String): String {
require(address.startsWith(P2P_PREFIX)) { "Failed to map topic: $address to a local address as it is not in the $P2P_PREFIX namespace" }
return PEERS_PREFIX + address.substring(P2P_PREFIX.length)
}
}
override val queueName: String = "$P2P_PREFIX${identity.toStringShort()}"

View File

@ -100,35 +100,43 @@ class ArtemisTcpTransport {
fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: MutualSslConfiguration?, enableSSL: Boolean = true): TransportConfiguration {
return p2pAcceptorTcpTransport(hostAndPort, config?.keyStore, config?.trustStore, enableSSL = enableSSL)
return p2pAcceptorTcpTransport(hostAndPort, config?.keyStore, config?.trustStore, enableSSL = enableSSL, useOpenSsl = config?.useOpenSsl ?: false)
}
fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: MutualSslConfiguration?, enableSSL: Boolean = true): TransportConfiguration {
fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: MutualSslConfiguration?, enableSSL: Boolean = true, keyStoreProvider: String? = null): TransportConfiguration {
return p2pConnectorTcpTransport(hostAndPort, config?.keyStore, config?.trustStore, enableSSL = enableSSL)
return p2pConnectorTcpTransport(hostAndPort, config?.keyStore, config?.trustStore, enableSSL = enableSSL, useOpenSsl = config?.useOpenSsl ?: false, keyStoreProvider = keyStoreProvider)
}
fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, keyStore: FileBasedCertificateStoreSupplier?, trustStore: FileBasedCertificateStoreSupplier?, enableSSL: Boolean = true): TransportConfiguration {
fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, keyStore: FileBasedCertificateStoreSupplier?, trustStore: FileBasedCertificateStoreSupplier?, enableSSL: Boolean = true, useOpenSsl: Boolean = false): TransportConfiguration {
val options = defaultArtemisOptions(hostAndPort).toMutableMap()
if (enableSSL) {
options.putAll(defaultSSLOptions)
(keyStore to trustStore).addToTransportOptions(options)
options[TransportConstants.SSL_PROVIDER] = if (useOpenSsl) TransportConstants.OPENSSL_PROVIDER else TransportConstants.DEFAULT_SSL_PROVIDER
}
options[TransportConstants.HANDSHAKE_TIMEOUT] = 0 // Suppress core.server.lambda$channelActive$0 - AMQ224088 error from load balancer type connections
return TransportConfiguration(acceptorFactoryClassName, options)
}
fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, keyStore: FileBasedCertificateStoreSupplier?, trustStore: FileBasedCertificateStoreSupplier?, enableSSL: Boolean = true): TransportConfiguration {
@Suppress("LongParameterList")
fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, keyStore: FileBasedCertificateStoreSupplier?, trustStore: FileBasedCertificateStoreSupplier?, enableSSL: Boolean = true, useOpenSsl: Boolean = false, keyStoreProvider: String? = null): TransportConfiguration {
val options = defaultArtemisOptions(hostAndPort).toMutableMap()
if (enableSSL) {
options.putAll(defaultSSLOptions)
(keyStore to trustStore).addToTransportOptions(options)
options[TransportConstants.SSL_PROVIDER] = if (useOpenSsl) TransportConstants.OPENSSL_PROVIDER else TransportConstants.DEFAULT_SSL_PROVIDER
keyStoreProvider?.let { options.put(TransportConstants.KEYSTORE_PROVIDER_PROP_NAME, keyStoreProvider) }
}
return TransportConfiguration(connectorFactoryClassName, options)
}
fun p2pConnectorTcpTransportFromList(hostAndPortList: List<NetworkHostAndPort>, config: MutualSslConfiguration?, enableSSL: Boolean = true, keyStoreProvider: String? = null): List<TransportConfiguration> = hostAndPortList.map {
p2pConnectorTcpTransport(it, config, enableSSL, keyStoreProvider)
}
fun rpcAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: BrokerRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration {
val options = defaultArtemisOptions(hostAndPort).toMutableMap()
@ -156,12 +164,17 @@ class ArtemisTcpTransport {
rpcConnectorTcpTransport(it, config, enableSSL)
}
fun rpcInternalClientTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration): TransportConfiguration {
return TransportConfiguration(connectorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions())
fun rpcInternalClientTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration, keyStoreProvider: String? = null): TransportConfiguration {
return TransportConfiguration(connectorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions() + asMap(keyStoreProvider))
}
fun rpcInternalAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration): TransportConfiguration {
return TransportConfiguration(acceptorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions() + (TransportConstants.HANDSHAKE_TIMEOUT to 0))
fun rpcInternalAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration, keyStoreProvider: String? = null): TransportConfiguration {
return TransportConfiguration(acceptorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions +
config.toTransportOptions() + (TransportConstants.HANDSHAKE_TIMEOUT to 0) + asMap(keyStoreProvider))
}
private fun asMap(keyStoreProvider: String?): Map<String, String> {
return keyStoreProvider?.let {mutableMapOf(TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to it)} ?: emptyMap()
}
}
}

View File

@ -1,5 +1,4 @@
@file:JvmName("ArtemisUtils")
package net.corda.nodeapi.internal
import java.nio.file.FileSystems
@ -16,3 +15,4 @@ fun Path.requireOnDefaultFileSystem() {
fun requireMessageSize(messageSize: Int, limit: Int) {
require(messageSize <= limit) { "Message exceeds maxMessageSize network parameter, maxMessageSize: [$limit], message size: [$messageSize]" }
}

View File

@ -0,0 +1,8 @@
package net.corda.nodeapi.internal
import org.apache.activemq.artemis.api.core.client.ClientSession
import org.apache.activemq.artemis.core.client.impl.ClientSessionInternal
fun ClientSession.stillOpen(): Boolean {
return (!isClosed && (this as? ClientSessionInternal)?.isClosing != false)
}

View File

@ -0,0 +1,17 @@
package net.corda.nodeapi.internal
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
/**
* A [ConcurrentBox] allows the implementation of track() with reduced contention. [concurrent] may be run from several
* threads (which means it MUST be threadsafe!), while [exclusive] stops the world until the tracking has been set up.
* Internally [ConcurrentBox] is implemented simply as a read-write lock.
*/
class ConcurrentBox<out T>(val content: T) {
val lock = ReentrantReadWriteLock()
inline fun <R> concurrent(block: T.() -> R): R = lock.read { block(content) }
inline fun <R> exclusive(block: T.() -> R): R = lock.write { block(content) }
}

View File

@ -0,0 +1,18 @@
package net.corda.nodeapi.internal
import org.apache.activemq.artemis.api.core.client.loadbalance.ConnectionLoadBalancingPolicy
/**
* Implementation of an Artemis load balancing policy. It does round-robin always starting from the first position, whereas
* the current [RoundRobinConnectionLoadBalancingPolicy] in Artemis picks the starting position randomly. This can lead to
* attempting to connect to an inactive broker on the first attempt, which can cause start-up delays depending on what connection
* settings are used.
*/
class RoundRobinConnectionPolicy : ConnectionLoadBalancingPolicy {
private var pos = 0
override fun select(max: Int): Int {
pos = if (pos >= max) 0 else pos
return pos++
}
}

View File

@ -1,5 +1,8 @@
@file:Suppress("TooGenericExceptionCaught") // needs to catch and handle/rethrow *all* exceptions in many places
package net.corda.nodeapi.internal.bridging
import com.google.common.util.concurrent.ThreadFactoryBuilder
import io.netty.channel.EventLoop
import io.netty.channel.EventLoopGroup
import io.netty.channel.nio.NioEventLoopGroup
import net.corda.core.identity.CordaX500Name
@ -11,11 +14,14 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_U
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders
import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress
import net.corda.nodeapi.internal.ArtemisSessionProvider
import net.corda.nodeapi.internal.ArtemisConstants.MESSAGE_ID_KEY
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.config.MutualSslConfiguration
import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPConfiguration
import net.corda.nodeapi.internal.protonwrapper.netty.ProxyConfig
import net.corda.nodeapi.internal.protonwrapper.netty.RevocationConfig
import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
import org.apache.activemq.artemis.api.core.client.ClientConsumer
@ -23,6 +29,10 @@ import org.apache.activemq.artemis.api.core.client.ClientMessage
import org.apache.activemq.artemis.api.core.client.ClientSession
import org.slf4j.MDC
import rx.Subscription
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
@ -34,33 +44,46 @@ import kotlin.concurrent.withLock
* The Netty thread pool used by the AMQPBridges is also shared and managed by the AMQPBridgeManager.
*/
@VisibleForTesting
class AMQPBridgeManager(config: MutualSslConfiguration,
maxMessageSize: Int,
crlCheckSoftFail: Boolean,
private val artemisMessageClientFactory: () -> ArtemisSessionProvider,
private val bridgeMetricsService: BridgeMetricsService? = null) : BridgeManager {
open class AMQPBridgeManager(keyStore: CertificateStore,
trustStore: CertificateStore,
useOpenSSL: Boolean,
proxyConfig: ProxyConfig? = null,
maxMessageSize: Int,
revocationConfig: RevocationConfig,
enableSNI: Boolean,
private val artemisMessageClientFactory: () -> ArtemisSessionProvider,
private val bridgeMetricsService: BridgeMetricsService? = null,
trace: Boolean,
sslHandshakeTimeout: Long?,
private val bridgeConnectionTTLSeconds: Int) : BridgeManager {
private val lock = ReentrantLock()
private val queueNamesToBridgesMap = mutableMapOf<String, MutableList<AMQPBridge>>()
private class AMQPConfigurationImpl private constructor(override val keyStore: CertificateStore,
override val trustStore: CertificateStore,
override val maxMessageSize: Int,
override val crlCheckSoftFail: Boolean) : AMQPConfiguration {
constructor(config: MutualSslConfiguration, maxMessageSize: Int, crlCheckSoftFail: Boolean) : this(config.keyStore.get(), config.trustStore.get(), maxMessageSize, crlCheckSoftFail)
private class AMQPConfigurationImpl(override val keyStore: CertificateStore,
override val trustStore: CertificateStore,
override val proxyConfig: ProxyConfig?,
override val maxMessageSize: Int,
override val revocationConfig: RevocationConfig,
override val useOpenSsl: Boolean,
override val enableSNI: Boolean,
override val sourceX500Name: String? = null,
override val trace: Boolean,
private val _sslHandshakeTimeout: Long?) : AMQPConfiguration {
override val sslHandshakeTimeout: Long
get() = _sslHandshakeTimeout ?: super.sslHandshakeTimeout
}
private val amqpConfig: AMQPConfiguration = AMQPConfigurationImpl(config, maxMessageSize, crlCheckSoftFail)
private val amqpConfig: AMQPConfiguration = AMQPConfigurationImpl(keyStore, trustStore, proxyConfig, maxMessageSize, revocationConfig,useOpenSSL, enableSNI, trace = trace, _sslHandshakeTimeout = sslHandshakeTimeout)
private var sharedEventLoopGroup: EventLoopGroup? = null
private var artemis: ArtemisSessionProvider? = null
constructor(config: MutualSslConfiguration,
p2pAddress: NetworkHostAndPort,
maxMessageSize: Int,
crlCheckSoftFail: Boolean) : this(config, maxMessageSize, crlCheckSoftFail, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) })
companion object {
private const val NUM_BRIDGE_THREADS = 0 // Default sized pool
private const val CORDA_NUM_BRIDGE_THREADS_PROP_NAME = "net.corda.nodeapi.amqpbridgemanager.NumBridgeThreads"
private val NUM_BRIDGE_THREADS = Integer.getInteger(CORDA_NUM_BRIDGE_THREADS_PROP_NAME, 0) // Default 0 means Netty default sized pool
private const val ARTEMIS_RETRY_BACKOFF = 5000L
}
/**
@ -71,13 +94,16 @@ class AMQPBridgeManager(config: MutualSslConfiguration,
* If the delivery fails the session is rolled back to prevent loss of the message. This may cause duplicate delivery,
* however Artemis and the remote Corda instanced will deduplicate these messages.
*/
private class AMQPBridge(val queueName: String,
@Suppress("TooManyFunctions")
private class AMQPBridge(val sourceX500Name: String,
val queueName: String,
val targets: List<NetworkHostAndPort>,
val legalNames: Set<CordaX500Name>,
private val amqpConfig: AMQPConfiguration,
sharedEventGroup: EventLoopGroup,
private val artemis: ArtemisSessionProvider,
private val bridgeMetricsService: BridgeMetricsService?) {
private val bridgeMetricsService: BridgeMetricsService?,
private val bridgeConnectionTTLSeconds: Int) {
companion object {
private val log = contextLogger()
}
@ -86,6 +112,7 @@ class AMQPBridgeManager(config: MutualSslConfiguration,
val oldMDC = MDC.getCopyOfContextMap() ?: emptyMap<String, String>()
try {
MDC.put("queueName", queueName)
MDC.put("source", amqpConfig.sourceX500Name)
MDC.put("targets", targets.joinToString(separator = ";") { it.toString() })
MDC.put("legalNames", legalNames.joinToString(separator = ";") { it.toString() })
MDC.put("maxMessageSize", amqpConfig.maxMessageSize.toString())
@ -106,10 +133,80 @@ class AMQPBridgeManager(config: MutualSslConfiguration,
private fun logWarnWithMDC(msg: String) = withMDC { log.warn(msg) }
val amqpClient = AMQPClient(targets, legalNames, amqpConfig, sharedThreadPool = sharedEventGroup)
private val lock = ReentrantLock() // lock to serialise session level access
private var session: ClientSession? = null
private var consumer: ClientConsumer? = null
private var connectedSubscription: Subscription? = null
@Volatile
private var messagesReceived: Boolean = false
private val eventLoop: EventLoop = sharedEventGroup.next()
private var artemisState: ArtemisState = ArtemisState.STOPPED
set(value) {
logDebugWithMDC { "State change $field to $value" }
field = value
}
@Suppress("MagicNumber")
private var artemisHeartbeatPlusBackoff = TimeUnit.SECONDS.toMillis(90)
private var amqpRestartEvent: ScheduledFuture<Unit>? = null
private var scheduledExecutorService: ScheduledExecutorService
= Executors.newSingleThreadScheduledExecutor(ThreadFactoryBuilder().setNameFormat("bridge-connection-reset-%d").build())
@Suppress("ClassNaming")
private sealed class ArtemisState {
object STARTING : ArtemisState()
data class STARTED(override val pending: ScheduledFuture<Unit>) : ArtemisState()
object CHECKING : ArtemisState()
object RESTARTED : ArtemisState()
object RECEIVING : ArtemisState()
object AMQP_STOPPED : ArtemisState()
object AMQP_STARTING : ArtemisState()
object AMQP_STARTED : ArtemisState()
object AMQP_RESTARTED : ArtemisState()
object STOPPING : ArtemisState()
object STOPPED : ArtemisState()
data class STOPPED_AMQP_START_SCHEDULED(override val pending: ScheduledFuture<Unit>) : ArtemisState()
open val pending: ScheduledFuture<Unit>? = null
override fun toString(): String = javaClass.simpleName
}
private fun artemis(inProgress: ArtemisState, block: (precedingState: ArtemisState) -> ArtemisState) {
val runnable = {
synchronized(artemis) {
try {
val precedingState = artemisState
artemisState.pending?.cancel(false)
artemisState = inProgress
artemisState = block(precedingState)
} catch (ex: Exception) {
withMDC { log.error("Unexpected error in Artemis processing in state $artemisState.", ex) }
}
}
}
if (eventLoop.inEventLoop()) {
runnable()
} else {
eventLoop.execute(runnable)
}
}
private fun scheduledArtemis(delay: Long, unit: TimeUnit, inProgress: ArtemisState, block: (precedingState: ArtemisState) -> ArtemisState): ScheduledFuture<Unit> {
return eventLoop.schedule<Unit>({
artemis(inProgress, block)
}, delay, unit)
}
private fun scheduledArtemisInExecutor(delay: Long, unit: TimeUnit, inProgress: ArtemisState, nextState: ArtemisState, block: () -> Unit): ScheduledFuture<Unit> {
return scheduledExecutorService.schedule<Unit>({
artemis(inProgress) {
nextState
}
block()
}, delay, unit)
}
fun start() {
logInfoWithMDC("Create new AMQP bridge")
@ -119,55 +216,196 @@ class AMQPBridgeManager(config: MutualSslConfiguration,
fun stop() {
logInfoWithMDC("Stopping AMQP bridge")
lock.withLock {
synchronized(artemis) {
consumer?.close()
consumer = null
session?.stop()
session = null
}
}
amqpClient.stop()
connectedSubscription?.unsubscribe()
connectedSubscription = null
}
private fun onSocketConnected(connected: Boolean) {
lock.withLock {
synchronized(artemis) {
if (connected) {
logInfoWithMDC("Bridge Connected")
bridgeMetricsService?.bridgeConnected(targets, legalNames)
val sessionFactory = artemis.started!!.sessionFactory
val session = sessionFactory.createSession(NODE_P2P_USER, NODE_P2P_USER, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
this.session = session
val consumer = session.createConsumer(queueName)
this.consumer = consumer
consumer.setMessageHandler(this@AMQPBridge::clientArtemisMessageHandler)
session.start()
} else {
logInfoWithMDC("Bridge Disconnected")
bridgeMetricsService?.bridgeDisconnected(targets, legalNames)
consumer?.close()
consumer = null
session?.stop()
artemis(ArtemisState.STOPPING) {
logInfoWithMDC("Stopping Artemis because stopping AMQP bridge")
closeConsumer()
consumer = null
eventLoop.execute {
artemis(ArtemisState.STOPPING) {
stopSession()
session = null
ArtemisState.STOPPED
}
}
ArtemisState.STOPPING
}
bridgeMetricsService?.bridgeDisconnected(targets, legalNames)
connectedSubscription?.unsubscribe()
connectedSubscription = null
// Do this last because we already scheduled the Artemis stop, so it's okay to unsubscribe onConnected first.
amqpClient.stop()
}
@Suppress("ComplexMethod")
private fun onSocketConnected(connected: Boolean) {
if (connected) {
logInfoWithMDC("Bridge Connected")
bridgeMetricsService?.bridgeConnected(targets, legalNames)
if (bridgeConnectionTTLSeconds > 0) {
// AMQP outbound connection will be restarted periodically with bridgeConnectionTTLSeconds interval
amqpRestartEvent = scheduledArtemisInExecutor(bridgeConnectionTTLSeconds.toLong(), TimeUnit.SECONDS,
ArtemisState.AMQP_STOPPED, ArtemisState.AMQP_RESTARTED) {
logInfoWithMDC("Bridge connection time to live exceeded. Restarting AMQP connection")
stopAndStartOutbound(ArtemisState.AMQP_RESTARTED)
}
}
artemis(ArtemisState.STARTING) {
val startedArtemis = artemis.started
if (startedArtemis == null) {
logInfoWithMDC("Bridge Connected but Artemis is disconnected")
ArtemisState.STOPPED
} else {
logInfoWithMDC("Bridge Connected so starting Artemis")
artemisHeartbeatPlusBackoff = startedArtemis.serverLocator.connectionTTL + ARTEMIS_RETRY_BACKOFF
try {
createSessionAndConsumer(startedArtemis)
ArtemisState.STARTED(scheduledArtemis(artemisHeartbeatPlusBackoff, TimeUnit.MILLISECONDS, ArtemisState.CHECKING) {
if (!messagesReceived) {
logInfoWithMDC("No messages received on new bridge. Restarting Artemis session")
if (restartSession()) {
ArtemisState.RESTARTED
} else {
logInfoWithMDC("Artemis session restart failed. Aborting by restarting AMQP connection.")
stopAndStartOutbound()
}
} else {
ArtemisState.RECEIVING
}
})
} catch (ex: Exception) {
// Now, bounce the AMQP connection to restart the sequence of establishing the connectivity back from the beginning.
withMDC { log.warn("Create Artemis start session error. Restarting AMQP connection", ex) }
stopAndStartOutbound()
}
}
}
} else {
logInfoWithMDC("Bridge Disconnected")
amqpRestartEvent?.cancel(false)
if (artemisState != ArtemisState.AMQP_STARTING && artemisState != ArtemisState.STOPPED) {
bridgeMetricsService?.bridgeDisconnected(targets, legalNames)
}
artemis(ArtemisState.STOPPING) { precedingState: ArtemisState ->
logInfoWithMDC("Stopping Artemis because AMQP bridge disconnected")
closeConsumer()
consumer = null
eventLoop.execute {
artemis(ArtemisState.STOPPING) {
stopSession()
session = null
when (precedingState) {
ArtemisState.AMQP_STOPPED ->
ArtemisState.STOPPED_AMQP_START_SCHEDULED(scheduledArtemis(artemisHeartbeatPlusBackoff,
TimeUnit.MILLISECONDS, ArtemisState.AMQP_STARTING) { startOutbound() })
ArtemisState.AMQP_RESTARTED -> {
artemis(ArtemisState.AMQP_STARTING) { startOutbound() }
ArtemisState.AMQP_STARTING
}
else -> ArtemisState.STOPPED
}
}
}
ArtemisState.STOPPING
}
}
}
private fun startOutbound(): ArtemisState {
logInfoWithMDC("Starting AMQP client")
amqpClient.start()
return ArtemisState.AMQP_STARTED
}
private fun stopAndStartOutbound(state: ArtemisState = ArtemisState.AMQP_STOPPED): ArtemisState {
amqpClient.stop()
// Bridge disconnect will detect this state and schedule an AMQP start.
return state
}
private fun createSessionAndConsumer(startedArtemis: ArtemisMessagingClient.Started): ClientSession {
logInfoWithMDC("Creating session and consumer.")
val sessionFactory = startedArtemis.sessionFactory
val session = sessionFactory.createSession(NODE_P2P_USER, NODE_P2P_USER, false, true,
true, false, DEFAULT_ACK_BATCH_SIZE)
this.session = session
// Several producers (in the case of shared bridge) can put messages in the same outbound p2p queue.
// The consumers are created using the source x500 name as a filter
val consumer = if (amqpConfig.enableSNI) {
session.createConsumer(queueName, "hyphenated_props:sender-subject-name = '${amqpConfig.sourceX500Name}'")
} else {
session.createConsumer(queueName)
}
this.consumer = consumer
session.start()
consumer.setMessageHandler(this@AMQPBridge::clientArtemisMessageHandler)
return session
}
private fun closeConsumer(): Boolean {
var closed = false
try {
consumer?.apply {
if (!isClosed) {
close()
}
}
closed = true
} catch (ex: Exception) {
withMDC { log.warn("Close artemis consumer error", ex) }
} finally {
return closed
}
}
private fun stopSession(): Boolean {
var stopped = false
try {
session?.apply {
if (!isClosed) {
stop()
}
}
stopped = true
} catch (ex: Exception) {
withMDC { log.warn("Stop Artemis session error", ex) }
} finally {
return stopped
}
}
private fun restartSession(): Boolean {
if (!stopSession()) {
// Session timed out stopping. The request/responses can be out of sequence on the session now, so abandon it.
session = null
// The consumer is also dead now too as attached to the dead session.
consumer = null
return false
}
try {
// Does not wait for a response.
this.session?.start()
} catch (ex: Exception) {
withMDC { log.error("Start Artemis session error", ex) }
}
return true
}
private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) {
messagesReceived = true
if (artemisMessage.bodySize > amqpConfig.maxMessageSize) {
val msg = "Message exceeds maxMessageSize network parameter, maxMessageSize: [${amqpConfig.maxMessageSize}], message size: [${artemisMessage.bodySize}], " +
"dropping message, uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}"
"dropping message, uuid: ${artemisMessage.getObjectProperty(MESSAGE_ID_KEY)}"
logWarnWithMDC(msg)
bridgeMetricsService?.packetDropEvent(artemisMessage, msg)
// Ack the message to prevent same message being sent to us again.
artemisMessage.individualAcknowledge()
try {
artemisMessage.individualAcknowledge()
} catch (ex: ActiveMQObjectClosedException) {
log.warn("Artemis message was closed")
}
return
}
val data = ByteArray(artemisMessage.bodySize).apply { artemisMessage.bodyBuffer.readBytes(this) }
val properties = HashMap<String, Any?>()
for (key in P2PMessagingHeaders.whitelistedHeaders) {
if (artemisMessage.containsProperty(key)) {
@ -178,18 +416,22 @@ class AMQPBridgeManager(config: MutualSslConfiguration,
properties[key] = value
}
}
logDebugWithMDC { "Bridged Send to ${legalNames.first()} uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}" }
logDebugWithMDC { "Bridged Send to ${legalNames.first()} uuid: ${artemisMessage.getObjectProperty(MESSAGE_ID_KEY)}" }
val peerInbox = translateLocalQueueToInboxAddress(queueName)
val sendableMessage = amqpClient.createMessage(data, peerInbox,
val sendableMessage = amqpClient.createMessage(artemisMessage.payload(), peerInbox,
legalNames.first().toString(),
properties)
sendableMessage.onComplete.then {
logDebugWithMDC { "Bridge ACK ${sendableMessage.onComplete.get()}" }
lock.withLock {
eventLoop.submit {
if (sendableMessage.onComplete.get() == MessageStatus.Acknowledged) {
artemisMessage.individualAcknowledge()
try {
artemisMessage.individualAcknowledge()
} catch (ex: ActiveMQObjectClosedException) {
log.warn("Artemis message was closed")
}
} else {
logInfoWithMDC("Rollback rejected message uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}")
logInfoWithMDC("Rollback rejected message uuid: ${artemisMessage.getObjectProperty(MESSAGE_ID_KEY)}")
// We need to commit any acknowledged messages before rolling back the failed
// (unacknowledged) message.
session?.commit()
@ -202,9 +444,9 @@ class AMQPBridgeManager(config: MutualSslConfiguration,
} catch (ex: IllegalStateException) {
// Attempting to send a message while the AMQP client is disconnected may cause message loss.
// The failed message is rolled back after committing acknowledged messages.
lock.withLock {
ex.message?.let { logInfoWithMDC(it)}
logInfoWithMDC("Rollback rejected message uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}")
eventLoop.submit {
ex.message?.let { logInfoWithMDC(it) }
logInfoWithMDC("Rollback rejected message uuid: ${artemisMessage.getObjectProperty(MESSAGE_ID_KEY)}")
session?.commit()
session?.rollback(false)
}
@ -213,20 +455,22 @@ class AMQPBridgeManager(config: MutualSslConfiguration,
}
}
override fun deployBridge(queueName: String, targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>) {
val newBridge = lock.withLock {
override fun deployBridge(sourceX500Name: String, queueName: String, targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>) {
lock.withLock {
val bridges = queueNamesToBridgesMap.getOrPut(queueName) { mutableListOf() }
for (target in targets) {
if (bridges.any { it.targets.contains(target) }) {
if (bridges.any { it.targets.contains(target) && it.sourceX500Name == sourceX500Name }) {
return
}
}
val newBridge = AMQPBridge(queueName, targets, legalNames, amqpConfig, sharedEventLoopGroup!!, artemis!!, bridgeMetricsService)
val newAMQPConfig = with(amqpConfig) { AMQPConfigurationImpl(keyStore, trustStore, proxyConfig, maxMessageSize,
revocationConfig, useOpenSsl, enableSNI, sourceX500Name, trace, sslHandshakeTimeout) }
val newBridge = AMQPBridge(sourceX500Name, queueName, targets, legalNames, newAMQPConfig, sharedEventLoopGroup!!, artemis!!,
bridgeMetricsService, bridgeConnectionTTLSeconds)
bridges += newBridge
bridgeMetricsService?.bridgeCreated(targets, legalNames)
newBridge
}
newBridge.start()
}.start()
}
override fun destroyBridge(queueName: String, targets: List<NetworkHostAndPort>) {
@ -246,6 +490,17 @@ class AMQPBridgeManager(config: MutualSslConfiguration,
}
}
fun destroyAllBridges(queueName: String): Map<String, BridgeEntry> {
return lock.withLock {
// queueNamesToBridgesMap returns a mutable list, .toList converts it to a immutable list so it won't be changed by the [destroyBridge] method.
val bridges = queueNamesToBridgesMap[queueName]?.toList()
destroyBridge(queueName, bridges?.flatMap { it.targets } ?: emptyList())
bridges?.map {
it.sourceX500Name to BridgeEntry(it.queueName, it.targets, it.legalNames.toList(), serviceAddress = false)
}?.toMap() ?: emptyMap()
}
}
override fun start() {
sharedEventLoopGroup = NioEventLoopGroup(NUM_BRIDGE_THREADS)
val artemis = artemisMessageClientFactory()

View File

@ -1,51 +1,124 @@
@file:Suppress("TooGenericExceptionCaught") // needs to catch and handle/rethrow *all* exceptions
package net.corda.nodeapi.internal.bridging
import net.corda.core.identity.CordaX500Name
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.ArtemisMessagingClient
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_CONTROL
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_NOTIFY
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
import net.corda.nodeapi.internal.ArtemisSessionProvider
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.config.MutualSslConfiguration
import net.corda.nodeapi.internal.crypto.x509
import net.corda.nodeapi.internal.protonwrapper.netty.ProxyConfig
import net.corda.nodeapi.internal.protonwrapper.netty.RevocationConfig
import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException
import org.apache.activemq.artemis.api.core.ActiveMQQueueExistsException
import org.apache.activemq.artemis.api.core.RoutingType
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ClientConsumer
import org.apache.activemq.artemis.api.core.client.ClientMessage
import org.apache.activemq.artemis.api.core.client.ClientSession
import rx.Observable
import rx.subjects.PublishSubject
import java.util.*
class BridgeControlListener(val config: MutualSslConfiguration,
class BridgeControlListener(private val keyStore: CertificateStore,
trustStore: CertificateStore,
useOpenSSL: Boolean,
proxyConfig: ProxyConfig? = null,
maxMessageSize: Int,
crlCheckSoftFail: Boolean,
revocationConfig: RevocationConfig,
enableSNI: Boolean,
private val artemisMessageClientFactory: () -> ArtemisSessionProvider,
bridgeMetricsService: BridgeMetricsService? = null) : AutoCloseable {
bridgeMetricsService: BridgeMetricsService? = null,
trace: Boolean = false,
sslHandshakeTimeout: Long? = null,
bridgeConnectionTTLSeconds: Int = 0) : AutoCloseable {
private val bridgeId: String = UUID.randomUUID().toString()
private val bridgeManager: BridgeManager = AMQPBridgeManager(
config,
maxMessageSize,
crlCheckSoftFail,
artemisMessageClientFactory,
bridgeMetricsService)
private var bridgeControlQueue = "$BRIDGE_CONTROL.$bridgeId"
private var bridgeNotifyQueue = "$BRIDGE_NOTIFY.$bridgeId"
private val validInboundQueues = mutableSetOf<String>()
private val bridgeManager = if (enableSNI) {
LoopbackBridgeManager(keyStore, trustStore, useOpenSSL, proxyConfig, maxMessageSize, revocationConfig, enableSNI,
artemisMessageClientFactory, bridgeMetricsService, this::validateReceiveTopic, trace, sslHandshakeTimeout,
bridgeConnectionTTLSeconds)
} else {
AMQPBridgeManager(keyStore, trustStore, useOpenSSL, proxyConfig, maxMessageSize, revocationConfig, enableSNI,
artemisMessageClientFactory, bridgeMetricsService, trace, sslHandshakeTimeout, bridgeConnectionTTLSeconds)
}
private var artemis: ArtemisSessionProvider? = null
private var controlConsumer: ClientConsumer? = null
private var notifyConsumer: ClientConsumer? = null
constructor(config: MutualSslConfiguration,
p2pAddress: NetworkHostAndPort,
maxMessageSize: Int,
revocationConfig: RevocationConfig,
enableSNI: Boolean,
proxy: ProxyConfig? = null) : this(config.keyStore.get(), config.trustStore.get(), config.useOpenSsl, proxy, maxMessageSize, revocationConfig, enableSNI, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) })
companion object {
private val log = contextLogger()
}
val active: Boolean
get() = validInboundQueues.isNotEmpty()
private val _activeChange = PublishSubject.create<Boolean>().toSerialized()
val activeChange: Observable<Boolean>
get() = _activeChange
private val _failure = PublishSubject.create<BridgeControlListener>().toSerialized()
val failure: Observable<BridgeControlListener>
get() = _failure
fun start() {
stop()
bridgeManager.start()
val artemis = artemisMessageClientFactory()
this.artemis = artemis
artemis.start()
val artemisClient = artemis.started!!
val artemisSession = artemisClient.session
val bridgeControlQueue = "$BRIDGE_CONTROL.$bridgeId"
artemisSession.createTemporaryQueue(BRIDGE_CONTROL, RoutingType.MULTICAST, bridgeControlQueue)
try {
stop()
val queueDisambiguityId = UUID.randomUUID().toString()
bridgeControlQueue = "$BRIDGE_CONTROL.$queueDisambiguityId"
bridgeNotifyQueue = "$BRIDGE_NOTIFY.$queueDisambiguityId"
bridgeManager.start()
val artemis = artemisMessageClientFactory()
this.artemis = artemis
artemis.start()
val artemisClient = artemis.started!!
val artemisSession = artemisClient.session
registerBridgeControlListener(artemisSession)
registerBridgeDuplicateChecker(artemisSession)
// Attempt to read available inboxes directly from Artemis before requesting updates from connected nodes
validInboundQueues.addAll(artemisSession.addressQuery(SimpleString("$P2P_PREFIX#")).queueNames.map { it.toString() })
log.info("Found inboxes: $validInboundQueues")
if (active) {
_activeChange.onNext(true)
}
val startupMessage = BridgeControl.BridgeToNodeSnapshotRequest(bridgeId).serialize(context = SerializationDefaults.P2P_CONTEXT)
.bytes
val bridgeRequest = artemisSession.createMessage(false)
bridgeRequest.writeBodyBufferBytes(startupMessage)
artemisClient.producer.send(BRIDGE_NOTIFY, bridgeRequest)
} catch (e: Exception) {
log.error("Failure to start BridgeControlListener", e)
_failure.onNext(this)
}
}
private fun registerBridgeControlListener(artemisSession: ClientSession) {
try {
artemisSession.createTemporaryQueue(BRIDGE_CONTROL, RoutingType.MULTICAST, bridgeControlQueue)
} catch (ex: ActiveMQQueueExistsException) {
// Ignore if there is a queue still not cleaned up
}
val control = artemisSession.createConsumer(bridgeControlQueue)
controlConsumer = control
control.setMessageHandler { msg ->
@ -53,22 +126,64 @@ class BridgeControlListener(val config: MutualSslConfiguration,
processControlMessage(msg)
} catch (ex: Exception) {
log.error("Unable to process bridge control message", ex)
_failure.onNext(this)
}
msg.acknowledge()
}
}
private fun registerBridgeDuplicateChecker(artemisSession: ClientSession) {
try {
artemisSession.createTemporaryQueue(BRIDGE_NOTIFY, RoutingType.MULTICAST, bridgeNotifyQueue)
} catch (ex: ActiveMQQueueExistsException) {
// Ignore if there is a queue still not cleaned up
}
val notify = artemisSession.createConsumer(bridgeNotifyQueue)
notifyConsumer = notify
notify.setMessageHandler { msg ->
try {
val data: ByteArray = ByteArray(msg.bodySize).apply { msg.bodyBuffer.readBytes(this) }
val notifyMessage = data.deserialize<BridgeControl.BridgeToNodeSnapshotRequest>(context = SerializationDefaults.P2P_CONTEXT)
if (notifyMessage.bridgeIdentity != bridgeId) {
log.error("Fatal Error! Two bridges have been configured simultaneously! Check the enterpriseConfiguration.externalBridge status")
System.exit(1)
}
} catch (ex: Exception) {
log.error("Unable to process bridge notification message", ex)
_failure.onNext(this)
}
msg.acknowledge()
}
val startupMessage = BridgeControl.BridgeToNodeSnapshotRequest(bridgeId).serialize(context = SerializationDefaults.P2P_CONTEXT).bytes
val bridgeRequest = artemisSession.createMessage(false)
bridgeRequest.writeBodyBufferBytes(startupMessage)
artemisClient.producer.send(BRIDGE_NOTIFY, bridgeRequest)
}
fun stop() {
validInboundQueues.clear()
controlConsumer?.close()
controlConsumer = null
artemis?.stop()
artemis = null
bridgeManager.stop()
try {
if (active) {
_activeChange.onNext(false)
}
validInboundQueues.clear()
controlConsumer?.close()
controlConsumer = null
notifyConsumer?.close()
notifyConsumer = null
artemis?.apply {
try {
started?.session?.deleteQueue(bridgeControlQueue)
} catch (e: ActiveMQNonExistentQueueException) {
log.warn("Queue $bridgeControlQueue does not exist and it can't be deleted")
}
try {
started?.session?.deleteQueue(bridgeNotifyQueue)
} catch (e: ActiveMQNonExistentQueueException) {
log.warn("Queue $bridgeNotifyQueue does not exist and it can't be deleted")
}
stop()
}
artemis = null
bridgeManager.stop()
} catch (e: Exception) {
log.error("Failure to stop BridgeControlListener", e)
}
}
override fun close() = stop()
@ -91,6 +206,10 @@ class BridgeControlListener(val config: MutualSslConfiguration,
log.info("Received bridge control message $controlMessage")
when (controlMessage) {
is BridgeControl.NodeToBridgeSnapshot -> {
if (!isConfigured(controlMessage.nodeIdentity)) {
log.error("Fatal error! Bridge not configured with keystore for node with legal name ${controlMessage.nodeIdentity}.")
System.exit(1)
}
if (!controlMessage.inboxQueues.all { validateInboxQueueName(it) }) {
log.error("Invalid queue names in control message $controlMessage")
return
@ -99,10 +218,20 @@ class BridgeControlListener(val config: MutualSslConfiguration,
log.error("Invalid queue names in control message $controlMessage")
return
}
for (outQueue in controlMessage.sendQueues) {
bridgeManager.deployBridge(outQueue.queueName, outQueue.targets, outQueue.legalNames.toSet())
}
val wasActive = active
validInboundQueues.addAll(controlMessage.inboxQueues)
for (outQueue in controlMessage.sendQueues) {
bridgeManager.deployBridge(controlMessage.nodeIdentity, outQueue.queueName, outQueue.targets, outQueue.legalNames.toSet())
}
log.info("Added inbox: ${controlMessage.inboxQueues}. Current inboxes: $validInboundQueues.")
if (bridgeManager is LoopbackBridgeManager) {
// Notify loopback bridge manager inboxes has changed.
bridgeManager.inboxesAdded(controlMessage.inboxQueues)
}
if (!wasActive && active) {
_activeChange.onNext(true)
}
}
is BridgeControl.BridgeToNodeSnapshotRequest -> {
log.error("Message from Bridge $controlMessage detected on wrong topic!")
@ -112,7 +241,7 @@ class BridgeControlListener(val config: MutualSslConfiguration,
log.error("Invalid queue names in control message $controlMessage")
return
}
bridgeManager.deployBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets, controlMessage.bridgeInfo.legalNames.toSet())
bridgeManager.deployBridge(controlMessage.nodeIdentity, controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets, controlMessage.bridgeInfo.legalNames.toSet())
}
is BridgeControl.Delete -> {
if (!controlMessage.bridgeInfo.queueName.startsWith(PEERS_PREFIX)) {
@ -121,7 +250,19 @@ class BridgeControlListener(val config: MutualSslConfiguration,
}
bridgeManager.destroyBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets)
}
is BridgeControl.BridgeHealthCheck -> {
log.warn("Not currently doing anything on BridgeHealthCheck")
return
}
}
}
private fun isConfigured(sourceX500Name: String): Boolean {
val keyStore = keyStore.value.internal
return keyStore.aliases().toList().any { alias ->
val x500Name = keyStore.getCertificate(alias).x509.subjectX500Principal
val cordaX500Name = CordaX500Name.build(x500Name)
cordaX500Name.toString() == sourceX500Name
}
}
}

View File

@ -11,7 +11,7 @@ import net.corda.core.utilities.NetworkHostAndPort
* @property legalNames The list of acceptable [CordaX500Name] names that should be presented as subject of the validated peer TLS certificate.
*/
@CordaSerializable
data class BridgeEntry(val queueName: String, val targets: List<NetworkHostAndPort>, val legalNames: List<CordaX500Name>)
data class BridgeEntry(val queueName: String, val targets: List<NetworkHostAndPort>, val legalNames: List<CordaX500Name>, val serviceAddress: Boolean)
sealed class BridgeControl {
/**
@ -47,4 +47,13 @@ sealed class BridgeControl {
*/
@CordaSerializable
data class Delete(val nodeIdentity: String, val bridgeInfo: BridgeEntry) : BridgeControl()
/**
* This message is sent to Bridge to check the health of it.
* @property requestId The identifier for the health check request as health check is likely to be produced repeatedly.
* @property command Allows to specify the sort fo health check that needs to be performed.
* @property bridgeInfo The connection details of the new bridge (optional).
*/
@CordaSerializable
data class BridgeHealthCheck(val requestId: Long, val command: String, val bridgeInfo: BridgeEntry?) : BridgeControl()
}

View File

@ -3,17 +3,20 @@ package net.corda.nodeapi.internal.bridging
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.VisibleForTesting
import net.corda.core.utilities.NetworkHostAndPort
import org.apache.activemq.artemis.api.core.client.ClientMessage
/**
* Provides an internal interface that the [BridgeControlListener] delegates to for Bridge activities.
*/
@VisibleForTesting
interface BridgeManager : AutoCloseable {
fun deployBridge(queueName: String, targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>)
fun deployBridge(sourceX500Name: String, queueName: String, targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>)
fun destroyBridge(queueName: String, targets: List<NetworkHostAndPort>)
fun start()
fun stop()
}
}
fun ClientMessage.payload() = ByteArray(bodySize).apply { bodyBuffer.readBytes(this) }

View File

@ -0,0 +1,223 @@
package net.corda.nodeapi.internal.bridging
import net.corda.nodeapi.internal.ConcurrentBox
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.VisibleForTesting
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateInboxAddressToLocalQueue
import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress
import net.corda.nodeapi.internal.ArtemisSessionProvider
import net.corda.nodeapi.internal.ArtemisConstants.MESSAGE_ID_KEY
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.protonwrapper.messages.impl.SendableMessageImpl
import net.corda.nodeapi.internal.protonwrapper.netty.ProxyConfig
import net.corda.nodeapi.internal.protonwrapper.netty.RevocationConfig
import net.corda.nodeapi.internal.stillOpen
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
import org.apache.activemq.artemis.api.core.client.ClientConsumer
import org.apache.activemq.artemis.api.core.client.ClientMessage
import org.apache.activemq.artemis.api.core.client.ClientProducer
import org.apache.activemq.artemis.api.core.client.ClientSession
import org.slf4j.MDC
/**
* The LoopbackBridgeManager holds the list of independent LoopbackBridge objects that actively loopback messages to local Artemis
* inboxes.
*/
@VisibleForTesting
class LoopbackBridgeManager(keyStore: CertificateStore,
trustStore: CertificateStore,
useOpenSSL: Boolean,
proxyConfig: ProxyConfig? = null,
maxMessageSize: Int,
revocationConfig: RevocationConfig,
enableSNI: Boolean,
private val artemisMessageClientFactory: () -> ArtemisSessionProvider,
private val bridgeMetricsService: BridgeMetricsService? = null,
private val isLocalInbox: (String) -> Boolean,
trace: Boolean,
sslHandshakeTimeout: Long? = null,
bridgeConnectionTTLSeconds: Int = 0) : AMQPBridgeManager(keyStore, trustStore, useOpenSSL, proxyConfig,
maxMessageSize, revocationConfig, enableSNI,
artemisMessageClientFactory, bridgeMetricsService,
trace, sslHandshakeTimeout,
bridgeConnectionTTLSeconds) {
companion object {
private val log = contextLogger()
}
private val queueNamesToBridgesMap = ConcurrentBox(mutableMapOf<String, MutableList<LoopbackBridge>>())
private var artemis: ArtemisSessionProvider? = null
/**
* Each LoopbackBridge is an independent consumer of messages from the Artemis local queue per designated endpoint.
* It attempts to loopback these messages via ArtemisClient to the local inbox.
*/
private class LoopbackBridge(val sourceX500Name: String,
val queueName: String,
val targets: List<NetworkHostAndPort>,
val legalNames: Set<CordaX500Name>,
artemis: ArtemisSessionProvider,
private val bridgeMetricsService: BridgeMetricsService?) {
companion object {
private val log = contextLogger()
}
// TODO: refactor MDC support, duplicated in AMQPBridgeManager.
private fun withMDC(block: () -> Unit) {
val oldMDC = MDC.getCopyOfContextMap()
try {
MDC.put("queueName", queueName)
MDC.put("source", sourceX500Name)
MDC.put("targets", targets.joinToString(separator = ";") { it.toString() })
MDC.put("legalNames", legalNames.joinToString(separator = ";") { it.toString() })
MDC.put("bridgeType", "loopback")
block()
} finally {
MDC.setContextMap(oldMDC)
}
}
private fun logDebugWithMDC(msg: () -> String) {
if (log.isDebugEnabled) {
withMDC { log.debug(msg()) }
}
}
private fun logInfoWithMDC(msg: String) = withMDC { log.info(msg) }
private fun logWarnWithMDC(msg: String) = withMDC { log.warn(msg) }
private val artemis = ConcurrentBox(artemis)
private var consumerSession: ClientSession? = null
private var producerSession: ClientSession? = null
private var consumer: ClientConsumer? = null
private var producer: ClientProducer? = null
fun start() {
logInfoWithMDC("Create new Artemis loopback bridge")
artemis.exclusive {
logInfoWithMDC("Bridge Connected")
bridgeMetricsService?.bridgeConnected(targets, legalNames)
val sessionFactory = started!!.sessionFactory
this@LoopbackBridge.consumerSession = sessionFactory.createSession(NODE_P2P_USER, NODE_P2P_USER, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
this@LoopbackBridge.producerSession = sessionFactory.createSession(NODE_P2P_USER, NODE_P2P_USER, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
// Several producers (in the case of shared bridge) can put messages in the same outbound p2p queue. The consumers are created using the source x500 name as a filter
val consumer = consumerSession!!.createConsumer(queueName, "hyphenated_props:sender-subject-name = '$sourceX500Name'")
consumer.setMessageHandler(this@LoopbackBridge::clientArtemisMessageHandler)
this@LoopbackBridge.consumer = consumer
this@LoopbackBridge.producer = producerSession!!.createProducer()
consumerSession?.start()
producerSession?.start()
}
}
fun stop() {
logInfoWithMDC("Stopping AMQP bridge")
artemis.exclusive {
bridgeMetricsService?.bridgeDisconnected(targets, legalNames)
consumer?.apply { if (!isClosed) close() }
consumer = null
producer?.apply { if (!isClosed) close() }
producer = null
consumerSession?.apply { if (stillOpen()) stop() }
consumerSession = null
producerSession?.apply { if (stillOpen()) stop()}
producerSession = null
}
}
private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) {
logDebugWithMDC { "Loopback Send to ${legalNames.first()} uuid: ${artemisMessage.getObjectProperty(MESSAGE_ID_KEY)}" }
val peerInbox = translateLocalQueueToInboxAddress(queueName)
producer?.send(SimpleString(peerInbox), artemisMessage) { artemisMessage.individualAcknowledge() }
bridgeMetricsService?.let { metricsService ->
val properties = ArtemisMessagingComponent.Companion.P2PMessagingHeaders.whitelistedHeaders.mapNotNull { key ->
if (artemisMessage.containsProperty(key)) {
key to artemisMessage.getObjectProperty(key).let { (it as? SimpleString)?.toString() ?: it }
} else {
null
}
}.toMap()
metricsService.packetAcceptedEvent(SendableMessageImpl(artemisMessage.payload(), peerInbox, legalNames.first().toString(), targets.first(), properties))
}
}
}
override fun deployBridge(sourceX500Name: String, queueName: String, targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>) {
val inboxAddress = translateLocalQueueToInboxAddress(queueName)
if (isLocalInbox(inboxAddress)) {
log.info("Deploying loopback bridge for $queueName, source $sourceX500Name")
queueNamesToBridgesMap.exclusive {
val bridges = getOrPut(queueName) { mutableListOf() }
for (target in targets) {
if (bridges.any { it.targets.contains(target) && it.sourceX500Name == sourceX500Name }) {
return
}
}
val newBridge = LoopbackBridge(sourceX500Name, queueName, targets, legalNames, artemis!!, bridgeMetricsService)
bridges += newBridge
bridgeMetricsService?.bridgeCreated(targets, legalNames)
newBridge
}.start()
} else {
log.info("Deploying AMQP bridge for $queueName, source $sourceX500Name")
super.deployBridge(sourceX500Name, queueName, targets, legalNames)
}
}
override fun destroyBridge(queueName: String, targets: List<NetworkHostAndPort>) {
super.destroyBridge(queueName, targets)
queueNamesToBridgesMap.exclusive {
val bridges = this[queueName] ?: mutableListOf()
for (target in targets) {
val bridge = bridges.firstOrNull { it.targets.contains(target) }
if (bridge != null) {
bridges -= bridge
if (bridges.isEmpty()) {
remove(queueName)
}
bridge.stop()
bridgeMetricsService?.bridgeDestroyed(bridge.targets, bridge.legalNames)
}
}
}
}
/**
* Remove any AMQP bridge for the local inbox and create a loopback bridge for that queue.
*/
fun inboxesAdded(inboxes: List<String>) {
for (inbox in inboxes) {
super.destroyAllBridges(translateInboxAddressToLocalQueue(inbox)).forEach { source, bridgeEntry ->
log.info("Destroyed AMQP Bridge '${bridgeEntry.queueName}', creating Loopback bridge for local inbox.")
deployBridge(source, bridgeEntry.queueName, bridgeEntry.targets, bridgeEntry.legalNames.toSet())
}
}
}
override fun start() {
super.start()
val artemis = artemisMessageClientFactory()
this.artemis = artemis
artemis.start()
}
override fun stop() = close()
override fun close() {
super.close()
queueNamesToBridgesMap.exclusive {
for (bridge in values.flatten()) {
bridge.stop()
}
clear()
artemis?.stop()
}
}
}

View File

@ -60,6 +60,7 @@ interface CertificateStore : Iterable<Pair<String, X509Certificate>> {
forEach { (alias, certificate) -> action.invoke(alias, certificate) }
}
fun aliases(): List<String> = value.internal.aliases().toList()
/**
* @throws IllegalArgumentException if no certificate for the alias is found, or if the certificate is not an [X509Certificate].
*/

View File

@ -1,4 +1,5 @@
@file:JvmName("ConfigUtilities")
@file:Suppress("LongParameterList")
package net.corda.nodeapi.internal.config
@ -52,21 +53,35 @@ const val CUSTOM_NODE_PROPERTIES_ROOT = "custom"
// This is to enable constructs like:
// `val keyStorePassword: String by config`
operator fun <T : Any> Config.getValue(receiver: Any, metadata: KProperty<*>): T {
return getValueInternal(metadata.name, metadata.returnType, UnknownConfigKeysPolicy.IGNORE::handle, nestedPath = null, baseDirectory = null)
return getValueInternal(
metadata.name,
metadata.returnType,
UnknownConfigKeysPolicy.IGNORE::handle,
nestedPath = null,
baseDirectory = null
)
}
// Problems:
// - Forces you to have a primary constructor with all fields of name and type matching the configuration file structure.
// - Encourages weak bean-like types.
// - Cannot support a many-to-one relationship between configuration file structures and configuration domain type. This is essential for versioning of the configuration files.
// - Cannot support a many-to-one relationship between configuration file structures and configuration domain type. This is essential for
// versioning of the configuration files.
// - It's complicated and based on reflection, meaning problems with it are typically found at runtime.
// - It doesn't support validation errors in a structured way. If something goes wrong, it throws exceptions, which doesn't support good usability practices like displaying all the errors at once.
fun <T : Any> Config.parseAs(clazz: KClass<T>, onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle,
nestedPath: String? = null, baseDirectory: Path? = null): T {
// - It doesn't support validation errors in a structured way. If something goes wrong, it throws exceptions, which doesn't support good
// usability practices like displaying all the errors at once.
fun <T : Any> Config.parseAs(
clazz: KClass<T>,
onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle,
nestedPath: String? = null,
baseDirectory: Path? = null
): T {
// Use custom parser if provided, instead of treating the object as data class.
clazz.findAnnotation<CustomConfigParser>()?.let { return uncheckedCast(it.parser.createInstance().parse(this)) }
require(clazz.isData) { "Only Kotlin data classes or class annotated with CustomConfigParser can be parsed. Offending: ${clazz.qualifiedName}" }
require(clazz.isData) {
"Only Kotlin data classes or class annotated with CustomConfigParser can be parsed. Offending: ${clazz.qualifiedName}"
}
val constructor = clazz.primaryConstructor!!
val parameters = constructor.parameters
val parameterNames = parameters.flatMap { param ->
@ -104,24 +119,46 @@ class UnknownConfigurationKeysException private constructor(val unknownKeys: Set
companion object {
fun of(offendingKeys: Set<String>): UnknownConfigurationKeysException = UnknownConfigurationKeysException(offendingKeys)
private fun message(offendingKeys: Set<String>) = "Unknown configuration keys: ${offendingKeys.joinToString(", ", "[", "]")}."
private fun message(offendingKeys: Set<String>) = "Unknown configuration keys: " +
"${offendingKeys.joinToString(", ", "[", "]")}."
}
}
inline fun <reified T : Any> Config.parseAs(noinline onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle): T = parseAs(T::class, onUnknownKeys)
inline fun <reified T : Any> Config.parseAs(
noinline onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle
): T = parseAs(T::class, onUnknownKeys)
fun Config.toProperties(): Properties {
return entrySet().associateByTo(
Properties(),
{ ConfigUtil.splitPath(it.key).joinToString(".") },
{ it.value.unwrapped().toString() })
{ it.value.unwrapped() })
}
private fun <T : Any> Config.getValueInternal(path: String, type: KType, onUnknownKeys: ((Set<String>, logger: Logger) -> Unit), nestedPath: String?, baseDirectory: Path?): T {
return uncheckedCast(if (type.arguments.isEmpty()) getSingleValue(path, type, onUnknownKeys, nestedPath, baseDirectory) else getCollectionValue(path, type, onUnknownKeys, nestedPath, baseDirectory))
private fun <T : Any> Config.getValueInternal(
path: String,
type: KType,
onUnknownKeys: ((Set<String>, logger: Logger) -> Unit),
nestedPath: String?,
baseDirectory: Path?
): T {
return uncheckedCast(
if (type.arguments.isEmpty()) {
getSingleValue(path, type, onUnknownKeys, nestedPath, baseDirectory)
} else {
getCollectionValue(path, type, onUnknownKeys, nestedPath, baseDirectory)
}
)
}
private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set<String>, logger: Logger) -> Unit, nestedPath: String?, baseDirectory: Path?): Any? {
@Suppress("ComplexMethod")
private fun Config.getSingleValue(
path: String,
type: KType,
onUnknownKeys: (Set<String>, logger: Logger) -> Unit,
nestedPath: String?,
baseDirectory: Path?
): Any? {
if (type.isMarkedNullable && !hasPath(path)) return null
val typeClass = type.jvmErasure
return try {
@ -153,7 +190,12 @@ private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set
else -> if (typeClass.java.isEnum) {
parseEnum(typeClass.java, getString(path))
} else {
getConfig(path).parseAs(typeClass, onUnknownKeys, nestedPath?.let { "$it.$path" } ?: path, baseDirectory = baseDirectory)
getConfig(path).parseAs(
typeClass,
onUnknownKeys,
nestedPath?.let { "$it.$path" } ?: path,
baseDirectory = baseDirectory
)
}
}
} catch (e: ConfigException.Missing) {
@ -164,7 +206,8 @@ private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set
private fun resolvePath(pathAsString: String, baseDirectory: Path?): Path {
val path = Paths.get(pathAsString)
return if (baseDirectory != null) {
// if baseDirectory been specified try resolving path against it. Note if `pathFromConfig` is an absolute path - this instruction has no effect.
// if baseDirectory been specified try resolving path against it. Note if `pathFromConfig` is an absolute path - this instruction
// has no effect.
baseDirectory.resolve(path)
} else {
path
@ -178,10 +221,18 @@ private fun ConfigException.Missing.relative(path: String, nestedPath: String?):
}
}
private fun Config.getCollectionValue(path: String, type: KType, onUnknownKeys: (Set<String>, logger: Logger) -> Unit, nestedPath: String?, baseDirectory: Path?): Collection<Any> {
@Suppress("ComplexMethod")
private fun Config.getCollectionValue(
path: String,
type: KType,
onUnknownKeys: (Set<String>, logger: Logger) -> Unit,
nestedPath: String?,
baseDirectory: Path?
): Collection<Any> {
val typeClass = type.jvmErasure
require(typeClass == List::class || typeClass == Set::class) { "$typeClass is not supported" }
val elementClass = type.arguments[0].type?.jvmErasure ?: throw IllegalArgumentException("Cannot work with star projection: $type")
val elementClass = type.arguments[0].type?.jvmErasure
?: throw IllegalArgumentException("Cannot work with star projection: $type")
if (!hasPath(path)) {
return if (typeClass == List::class) emptyList() else emptySet()
}
@ -240,7 +291,13 @@ private fun <T : Enum<T>> enumBridge(clazz: Class<T>, name: String): T {
*/
fun Any.toConfig(): Config = ConfigValueFactory.fromMap(toConfigMap()).toConfig()
fun Any?.toConfigValue(): ConfigValue = if (this is ConfigValue) this else if (this != null) ConfigValueFactory.fromAnyRef(convertValue(this)) else ConfigValueFactory.fromAnyRef(null)
fun Any?.toConfigValue(): ConfigValue = if (this is ConfigValue) {
this
} else if (this != null) {
ConfigValueFactory.fromAnyRef(convertValue(this))
} else {
ConfigValueFactory.fromAnyRef(null)
}
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
// Reflect over the fields of the receiver and generate a value Map that can use to create Config object.
@ -253,7 +310,8 @@ private fun Any.toConfigMap(): Map<String, Any> {
val configValue = if (value is String || value is Boolean || value is Number) {
// These types are supported by Config as use as is
value
} else if (value is Temporal || value is NetworkHostAndPort || value is CordaX500Name || value is Path || value is URL || value is UUID || value is X500Principal) {
} else if (value is Temporal || value is NetworkHostAndPort || value is CordaX500Name ||
value is Path || value is URL || value is UUID || value is X500Principal) {
// These types make sense to be represented as Strings and the exact inverse parsing function for use in parseAs
value.toString()
} else if (value is Enum<*>) {
@ -278,7 +336,8 @@ private fun convertValue(value: Any): Any {
return if (value is String || value is Boolean || value is Number) {
// These types are supported by Config as use as is
value
} else if (value is Temporal || value is NetworkHostAndPort || value is CordaX500Name || value is Path || value is URL || value is UUID || value is X500Principal) {
} else if (value is Temporal || value is NetworkHostAndPort || value is CordaX500Name ||
value is Path || value is URL || value is UUID || value is X500Principal) {
// These types make sense to be represented as Strings and the exact inverse parsing function for use in parseAs
value.toString()
} else if (value is Enum<*>) {

View File

@ -0,0 +1,63 @@
package net.corda.nodeapi.internal.config
import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds
import java.time.Duration
/**
* Predefined connection configurations used by Artemis clients (currently used in the P2P messaging layer).
* The enum names represent the approximate total duration of the failover (with exponential back-off). The formula used to calculate
* this duration is as follows:
*
* totalFailoverDuration = SUM(k=0 to [reconnectAttempts]) of [retryInterval] * POW([retryIntervalMultiplier], k)
*
* Example calculation for [DEFAULT]:
*
* totalFailoverDuration = 5 + 5 * 1.5 + 5 * (1.5)^2 + 5 * (1.5)^3 + 5 * (1.5)^4 = ~66 seconds
*
* @param failoverOnInitialAttempt Determines whether failover is triggered if initial connection fails.
* @param initialConnectAttempts The number of reconnect attempts if failover is enabled for initial connection. A value
* of -1 represents infinite attempts.
* @param reconnectAttempts The number of reconnect attempts for failover after initial connection is done. A value
* of -1 represents infinite attempts.
* @param retryInterval Duration between reconnect attempts.
* @param retryIntervalMultiplier Value used in the reconnection back-off process.
* @param maxRetryInterval Determines the maximum duration between reconnection attempts. Useful when using infinite retries.
*/
enum class MessagingServerConnectionConfiguration {
DEFAULT {
override fun failoverOnInitialAttempt(isHa: Boolean) = true
override fun initialConnectAttempts(isHa: Boolean) = 5
override fun reconnectAttempts(isHa: Boolean) = 5
override fun retryInterval() = 5.seconds
override fun retryIntervalMultiplier() = 1.5
override fun maxRetryInterval(isHa: Boolean) = 3.minutes
},
FAIL_FAST {
override fun failoverOnInitialAttempt(isHa: Boolean) = isHa
override fun initialConnectAttempts(isHa: Boolean) = 0
// Client die too fast during failover/failback, need a few reconnect attempts to allow new master to become active
override fun reconnectAttempts(isHa: Boolean) = if (isHa) 3 else 0
override fun retryInterval() = 5.seconds
override fun retryIntervalMultiplier() = 1.5
override fun maxRetryInterval(isHa: Boolean) = 3.minutes
},
CONTINUOUS_RETRY {
override fun failoverOnInitialAttempt(isHa: Boolean) = true
override fun initialConnectAttempts(isHa: Boolean) = if (isHa) 0 else -1
override fun reconnectAttempts(isHa: Boolean) = -1
override fun retryInterval() = 5.seconds
override fun retryIntervalMultiplier() = 1.5
override fun maxRetryInterval(isHa: Boolean) = if (isHa) 3.minutes else 5.minutes
};
abstract fun failoverOnInitialAttempt(isHa: Boolean): Boolean
abstract fun initialConnectAttempts(isHa: Boolean): Int
abstract fun reconnectAttempts(isHa: Boolean): Int
abstract fun retryInterval(): Duration
abstract fun retryIntervalMultiplier(): Double
abstract fun maxRetryInterval(isHa: Boolean): Duration
}

View File

@ -4,6 +4,7 @@ interface SslConfiguration {
val keyStore: FileBasedCertificateStoreSupplier?
val trustStore: FileBasedCertificateStoreSupplier?
val useOpenSsl: Boolean
companion object {
@ -19,4 +20,10 @@ interface MutualSslConfiguration : SslConfiguration {
override val trustStore: FileBasedCertificateStoreSupplier
}
private class MutualSslOptions(override val keyStore: FileBasedCertificateStoreSupplier, override val trustStore: FileBasedCertificateStoreSupplier) : MutualSslConfiguration
private class MutualSslOptions(override val keyStore: FileBasedCertificateStoreSupplier,
override val trustStore: FileBasedCertificateStoreSupplier) : MutualSslConfiguration {
override val useOpenSsl: Boolean = false
}
const val DEFAULT_SSL_HANDSHAKE_TIMEOUT_MILLIS = 60000L // Set at least 3 times higher than sun.security.provider.certpath.URICertStore.DEFAULT_CRL_CONNECT_TIMEOUT which is 15 sec

View File

@ -61,11 +61,6 @@ interface CryptoService : SignOnlyCryptoService {
*/
fun generateKeyPair(alias: String, scheme: SignatureScheme): PublicKey
/**
* Returns the type of the service.
*/
fun getType(): SupportedCryptoServices
// ******************************************************
// ENTERPRISE ONLY CODE FOR WRAPPING KEYS API STARTS HERE

View File

@ -17,7 +17,6 @@ import net.corda.nodeapi.internal.crypto.save
import net.corda.nodeapi.internal.cryptoservice.*
import net.corda.nodeapi.internal.cryptoservice.CryptoService
import net.corda.nodeapi.internal.cryptoservice.CryptoServiceException
import net.corda.nodeapi.internal.cryptoservice.SupportedCryptoServices
import org.bouncycastle.operator.ContentSigner
import java.nio.file.Path
import java.security.*
@ -41,8 +40,6 @@ class BCCryptoService(private val legalName: X500Principal,
val detailedLogger = detailedLogger()
}
override fun getType(): SupportedCryptoServices = SupportedCryptoServices.BC_SIMPLE
// TODO check if keyStore exists.
// TODO make it private when E2ETestKeyManagementService does not require direct access to the private key.
var certificateStore: CertificateStore = certificateStoreSupplier.get(true)

View File

@ -2,9 +2,9 @@ package net.corda.nodeapi.internal.network
import com.typesafe.config.Config
import net.corda.common.configuration.parsing.internal.Configuration
import net.corda.common.configuration.parsing.internal.get
import net.corda.common.configuration.parsing.internal.mapValid
import net.corda.common.configuration.parsing.internal.nested
import net.corda.common.configuration.parsing.internal.withOptions
import net.corda.common.validation.internal.Validated
import net.corda.core.internal.noPackageOverlap
import net.corda.core.internal.requirePackageValid
@ -17,7 +17,7 @@ import java.security.KeyStoreException
typealias Valid<TARGET> = Validated<TARGET, Configuration.Validation.Error>
fun Config.parseAsNetworkParametersConfiguration(options: Configuration.Validation.Options = Configuration.Validation.Options(strict = false)):
fun Config.parseAsNetworkParametersConfiguration(options: Configuration.Options = Configuration.Options.defaults):
Valid<NetworkParametersOverrides> = NetworkParameterOverridesSpec.parse(this, options)
internal fun <T> badValue(msg: String): Valid<T> = Validated.invalid(sequenceOf(Configuration.Validation.Error.BadValue.of(msg)).toSet())
@ -36,11 +36,12 @@ internal object NetworkParameterOverridesSpec : Configuration.Specification<Netw
private val keystorePassword by string()
private val keystoreAlias by string()
override fun parseValid(configuration: Config): Validated<PackageOwner, Configuration.Validation.Error> {
val suppliedKeystorePath = configuration[keystore]
val keystorePassword = configuration[keystorePassword]
override fun parseValid(configuration: Config, options: Configuration.Options): Validated<PackageOwner, Configuration.Validation.Error> {
val config = configuration.withOptions(options)
val suppliedKeystorePath = config[keystore]
val keystorePassword = config[keystorePassword]
return try {
val javaPackageName = configuration[packageName]
val javaPackageName = config[packageName]
val absoluteKeystorePath = if (suppliedKeystorePath.isAbsolute) {
suppliedKeystorePath
} else {
@ -49,10 +50,10 @@ internal object NetworkParameterOverridesSpec : Configuration.Specification<Netw
}.toAbsolutePath()
val ks = loadKeyStore(absoluteKeystorePath, keystorePassword)
return try {
val publicKey = ks.getCertificate(configuration[keystoreAlias]).publicKey
val publicKey = ks.getCertificate(config[keystoreAlias]).publicKey
valid(PackageOwner(javaPackageName, publicKey))
} catch (kse: KeyStoreException) {
badValue("Keystore has not been initialized for alias ${configuration[keystoreAlias]}.")
badValue("Keystore has not been initialized for alias ${config[keystoreAlias]}")
}
} catch (kse: KeyStoreException) {
badValue("Password is incorrect or the key store is damaged for keyStoreFilePath: $suppliedKeystorePath.")
@ -79,8 +80,9 @@ internal object NetworkParameterOverridesSpec : Configuration.Specification<Netw
}
}
override fun parseValid(configuration: Config): Valid<NetworkParametersOverrides> {
val packageOwnership = configuration[packageOwnership]
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<NetworkParametersOverrides> {
val config = configuration.withOptions(options)
val packageOwnership = config[packageOwnership]
if (packageOwnership != null && !noPackageOverlap(packageOwnership.map { it.javaPackageName })) {
return Validated.invalid(sequenceOf(Configuration.Validation.Error.BadValue.of(
"Package namespaces must not overlap",
@ -89,11 +91,11 @@ internal object NetworkParameterOverridesSpec : Configuration.Specification<Netw
)).toSet())
}
return valid(NetworkParametersOverrides(
minimumPlatformVersion = configuration[minimumPlatformVersion],
maxMessageSize = configuration[maxMessageSize],
maxTransactionSize = configuration[maxTransactionSize],
minimumPlatformVersion = config[minimumPlatformVersion],
maxMessageSize = config[maxMessageSize],
maxTransactionSize = config[maxTransactionSize],
packageOwnership = packageOwnership,
eventHorizon = configuration[eventHorizon]
eventHorizon = config[eventHorizon]
))
}

View File

@ -45,7 +45,11 @@ internal class ConnectionStateMachine(private val serverMode: Boolean,
userName: String?,
password: String?) : BaseHandler() {
companion object {
private const val IDLE_TIMEOUT = 10000
private const val CORDA_AMQP_FRAME_SIZE_PROP_NAME = "net.corda.nodeapi.connectionstatemachine.AmqpMaxFrameSize"
private const val CORDA_AMQP_IDLE_TIMEOUT_PROP_NAME = "net.corda.nodeapi.connectionstatemachine.AmqpIdleTimeout"
private val MAX_FRAME_SIZE = Integer.getInteger(CORDA_AMQP_FRAME_SIZE_PROP_NAME, 128 * 1024)
private val IDLE_TIMEOUT = Integer.getInteger(CORDA_AMQP_IDLE_TIMEOUT_PROP_NAME, 10 * 1000)
private val log = contextLogger()
}
@ -102,6 +106,7 @@ internal class ConnectionStateMachine(private val serverMode: Boolean,
transport.context = connection
@Suppress("UsePropertyAccessSyntax")
transport.setEmitFlowEventOnSend(true)
transport.maxFrameSize = MAX_FRAME_SIZE
connection.collect(collector)
val sasl = transport.sasl()
if (userName != null) {
@ -488,7 +493,9 @@ internal class ConnectionStateMachine(private val serverMode: Boolean,
}
fun transportWriteMessage(msg: SendableMessageImpl) {
msg.buf = encodePayloadBytes(msg)
val encoded = encodePayloadBytes(msg)
msg.release()
msg.buf = encoded
val messageQueue = messageQueues.getOrPut(msg.topic, { LinkedList() })
messageQueue.offer(msg)
if (session != null) {

View File

@ -38,7 +38,9 @@ internal class EventProcessor(private val channel: Channel,
userName: String?,
password: String?) {
companion object {
private const val FLOW_WINDOW_SIZE = 10
private const val CORDA_AMQP_FLOW_WINDOW_SIZE_PROP_NAME = "net.corda.nodeapi.eventprocessor.FlowWindowSize"
private val FLOW_WINDOW_SIZE = Integer.getInteger(CORDA_AMQP_FLOW_WINDOW_SIZE_PROP_NAME, 5)
private val log = contextLogger()
}

View File

@ -11,4 +11,5 @@ interface ApplicationMessage {
val destinationLegalName: String
val destinationLink: NetworkHostAndPort
val applicationProperties: Map<String, Any?>
fun release()
}

View File

@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.protonwrapper.messages.impl
import io.netty.channel.Channel
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus
import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
import org.apache.qpid.proton.engine.Delivery
@ -10,7 +11,7 @@ import org.apache.qpid.proton.engine.Delivery
* An internal packet management class that allows tracking of asynchronous acknowledgements
* that in turn send Delivery messages back to the originator.
*/
internal class ReceivedMessageImpl(override val payload: ByteArray,
internal class ReceivedMessageImpl(override var payload: ByteArray,
override val topic: String,
override val sourceLegalName: String,
override val sourceLink: NetworkHostAndPort,
@ -19,11 +20,25 @@ internal class ReceivedMessageImpl(override val payload: ByteArray,
override val applicationProperties: Map<String, Any?>,
private val channel: Channel,
private val delivery: Delivery) : ReceivedMessage {
companion object {
private val emptyPayload = ByteArray(0)
private val logger = contextLogger()
}
data class MessageCompleter(val status: MessageStatus, val delivery: Delivery)
override fun release() {
payload = emptyPayload
}
override fun complete(accepted: Boolean) {
release()
val status = if (accepted) MessageStatus.Acknowledged else MessageStatus.Rejected
channel.writeAndFlush(MessageCompleter(status, delivery))
if (channel.isActive) {
channel.writeAndFlush(MessageCompleter(status, delivery))
} else {
logger.info("Not writing $status as $channel is not active")
}
}
override fun toString(): String = "Received ${String(payload)} $topic"

View File

@ -11,11 +11,15 @@ import net.corda.nodeapi.internal.protonwrapper.messages.SendableMessage
* An internal packet management class that allows handling of the encoded buffers and
* allows registration of an acknowledgement handler when the remote receiver confirms durable storage.
*/
internal class SendableMessageImpl(override val payload: ByteArray,
internal class SendableMessageImpl(override var payload: ByteArray,
override val topic: String,
override val destinationLegalName: String,
override val destinationLink: NetworkHostAndPort,
override val applicationProperties: Map<String, Any?>) : SendableMessage {
companion object {
private val emptyPayload = ByteArray(0)
}
var buf: ByteBuf? = null
@Volatile
var status: MessageStatus = MessageStatus.Unsent
@ -23,12 +27,14 @@ internal class SendableMessageImpl(override val payload: ByteArray,
private val _onComplete = openFuture<MessageStatus>()
override val onComplete: CordaFuture<MessageStatus> get() = _onComplete
fun release() {
override fun release() {
payload = emptyPayload
buf?.release()
buf = null
}
fun doComplete(status: MessageStatus) {
release()
this.status = status
_onComplete.set(status)
}

View File

@ -5,11 +5,16 @@ import io.netty.channel.ChannelDuplexHandler
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelPromise
import io.netty.channel.socket.SocketChannel
import io.netty.handler.proxy.ProxyConnectException
import io.netty.handler.proxy.ProxyConnectionEvent
import io.netty.handler.ssl.SniCompletionEvent
import io.netty.handler.ssl.SslHandler
import io.netty.handler.ssl.SslHandshakeCompletionEvent
import io.netty.util.ReferenceCountUtil
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.trace
import net.corda.nodeapi.internal.ArtemisConstants.MESSAGE_ID_KEY
import net.corda.nodeapi.internal.crypto.x509
import net.corda.nodeapi.internal.protonwrapper.engine.EventProcessor
import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
@ -23,6 +28,8 @@ import org.slf4j.MDC
import java.net.InetSocketAddress
import java.nio.channels.ClosedChannelException
import java.security.cert.X509Certificate
import javax.net.ssl.ExtendedSSLSession
import javax.net.ssl.SNIHostName
import javax.net.ssl.SSLException
/**
@ -30,23 +37,29 @@ import javax.net.ssl.SSLException
* It also add some extra checks to the SSL handshake to support our non-standard certificate checks of legal identity.
* When a valid SSL connections is made then it initialises a proton-j engine instance to handle the protocol layer.
*/
@Suppress("TooManyFunctions")
internal class AMQPChannelHandler(private val serverMode: Boolean,
private val allowedRemoteLegalNames: Set<CordaX500Name>?,
private val keyManagerFactoriesMap: Map<String, CertHoldingKeyManagerFactoryWrapper>,
private val userName: String?,
private val password: String?,
private val trace: Boolean,
private val onOpen: (Pair<SocketChannel, ConnectionChange>) -> Unit,
private val onClose: (Pair<SocketChannel, ConnectionChange>) -> Unit,
private val suppressLogs: Boolean,
private val onOpen: (SocketChannel, ConnectionChange) -> Unit,
private val onClose: (SocketChannel, ConnectionChange) -> Unit,
private val onReceive: (ReceivedMessage) -> Unit) : ChannelDuplexHandler() {
companion object {
private val log = contextLogger()
const val PROXY_LOGGER_NAME = "preProxyLogger"
}
private lateinit var remoteAddress: InetSocketAddress
private var localCert: X509Certificate? = null
private var remoteCert: X509Certificate? = null
private var eventProcessor: EventProcessor? = null
private var suppressClose: Boolean = false
private var badCert: Boolean = false
private var localCert: X509Certificate? = null
private var requestedServerName: String? = null
private fun withMDC(block: () -> Unit) {
val oldMDC = MDC.getCopyOfContextMap() ?: emptyMap<String, String>()
@ -62,39 +75,50 @@ internal class AMQPChannelHandler(private val serverMode: Boolean,
}
}
private fun logDebugWithMDC(msg: () -> String) {
if (log.isDebugEnabled) {
withMDC { log.debug(msg()) }
private fun logDebugWithMDC(msgFn: () -> String) {
if (!suppressLogs) {
if (log.isDebugEnabled) {
withMDC { log.debug(msgFn()) }
}
} else {
withMDC { log.trace(msgFn) }
}
}
private fun logInfoWithMDC(msg: String) = withMDC { log.info(msg) }
private fun logInfoWithMDC(msgFn: () -> String) {
if (!suppressLogs) {
if (log.isInfoEnabled) {
withMDC { log.info(msgFn()) }
}
} else {
withMDC { log.trace(msgFn) }
}
}
private fun logWarnWithMDC(msg: String) = withMDC { log.warn(msg) }
private fun logErrorWithMDC(msg: String, ex: Throwable? = null) = withMDC { log.error(msg, ex) }
private fun logWarnWithMDC(msg: String) = withMDC { if (!suppressLogs) log.warn(msg) else log.trace { msg } }
private fun logErrorWithMDC(msg: String, ex: Throwable? = null) = withMDC { if (!suppressLogs) log.error(msg, ex) else log.trace(msg, ex) }
override fun channelActive(ctx: ChannelHandlerContext) {
val ch = ctx.channel()
remoteAddress = ch.remoteAddress() as InetSocketAddress
val localAddress = ch.localAddress() as InetSocketAddress
logInfoWithMDC("New client connection ${ch.id()} from $remoteAddress to $localAddress")
logInfoWithMDC { "New client connection ${ch.id()} from $remoteAddress to $localAddress" }
}
private fun createAMQPEngine(ctx: ChannelHandlerContext) {
val ch = ctx.channel()
eventProcessor = EventProcessor(ch, serverMode, localCert!!.subjectX500Principal.toString(), remoteCert!!.subjectX500Principal.toString(), userName, password)
val connection = eventProcessor!!.connection
val transport = connection.transport as ProtonJTransport
if (trace) {
val connection = eventProcessor!!.connection
val transport = connection.transport as ProtonJTransport
transport.protocolTracer = object : ProtocolTracer {
override fun sentFrame(transportFrame: TransportFrame) {
logInfoWithMDC("${transportFrame.body}")
logInfoWithMDC { "${transportFrame.body}" }
}
override fun receivedFrame(transportFrame: TransportFrame) {
logInfoWithMDC("${transportFrame.body}")
logInfoWithMDC { "${transportFrame.body}" }
}
}
}
@ -104,51 +128,60 @@ internal class AMQPChannelHandler(private val serverMode: Boolean,
override fun channelInactive(ctx: ChannelHandlerContext) {
val ch = ctx.channel()
logInfoWithMDC("Closed client connection ${ch.id()} from $remoteAddress to ${ch.localAddress()}")
onClose(Pair(ch as SocketChannel, ConnectionChange(remoteAddress, remoteCert, false, badCert)))
logInfoWithMDC { "Closed client connection ${ch.id()} from $remoteAddress to ${ch.localAddress()}" }
if (!suppressClose) {
onClose(ch as SocketChannel, ConnectionChange(remoteAddress, remoteCert, false, badCert))
}
eventProcessor?.close()
ctx.fireChannelInactive()
}
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
if (evt is SslHandshakeCompletionEvent) {
if (evt.isSuccess) {
val sslHandler = ctx.pipeline().get(SslHandler::class.java)
localCert = sslHandler.engine().session.localCertificates[0].x509
remoteCert = sslHandler.engine().session.peerCertificates[0].x509
val remoteX500Name = try {
CordaX500Name.build(remoteCert!!.subjectX500Principal)
} catch (ex: IllegalArgumentException) {
badCert = true
logErrorWithMDC("Certificate subject not a valid CordaX500Name", ex)
ctx.close()
return
when (evt) {
is ProxyConnectionEvent -> {
if (trace) {
log.info("ProxyConnectionEvent received: $evt")
try {
ctx.pipeline().remove(PROXY_LOGGER_NAME)
} catch (ex: NoSuchElementException) {
// ignore
}
}
if (allowedRemoteLegalNames != null && remoteX500Name !in allowedRemoteLegalNames) {
badCert = true
logErrorWithMDC("Provided certificate subject $remoteX500Name not in expected set $allowedRemoteLegalNames")
ctx.close()
return
}
logInfoWithMDC("Handshake completed with subject: $remoteX500Name")
createAMQPEngine(ctx)
onOpen(Pair(ctx.channel() as SocketChannel, ConnectionChange(remoteAddress, remoteCert, true, false)))
} else {
val cause = evt.cause()
// This happens when the peer node is closed during SSL establishment.
if (cause is ClosedChannelException) {
logWarnWithMDC("SSL Handshake closed early.")
} else if (cause is SSLException && cause.message == "handshake timed out") { // Sadly the exception thrown by Netty wrapper requires that we check the message.
logWarnWithMDC("SSL Handshake timed out")
} else {
badCert = true
}
logErrorWithMDC("Handshake failure ${evt.cause().message}")
if (log.isTraceEnabled) {
withMDC { log.trace("Handshake failure", evt.cause()) }
}
ctx.close()
// update address to the real target address
remoteAddress = evt.destinationAddress()
}
is SniCompletionEvent -> {
if (evt.isSuccess) {
// The SniCompletionEvent is fired up before context is switched (after SslHandshakeCompletionEvent)
// so we save the requested server name now to be able log it once the handshake is completed successfully
// Note: this event is only triggered when using OpenSSL.
requestedServerName = evt.hostname()
logInfoWithMDC { "SNI completion success." }
} else {
logErrorWithMDC("SNI completion failure: ${evt.cause().message}")
}
}
is SslHandshakeCompletionEvent -> {
if (evt.isSuccess) {
handleSuccessfulHandshake(ctx)
} else {
handleFailedHandshake(ctx, evt)
}
}
}
}
private fun SslHandler.getRequestedServerName(): String? {
return if (serverMode) {
val session = engine().session
when (session) {
// Server name can be obtained from SSL session when using JavaSSL.
is ExtendedSSLSession -> (session.requestedServerNames.firstOrNull() as? SNIHostName)?.asciiName
// For Open SSL server name is obtained from SniCompletionEvent
else -> requestedServerName
}
} else {
(engine().sslParameters?.serverNames?.firstOrNull() as? SNIHostName)?.asciiName
}
}
@ -158,6 +191,10 @@ internal class AMQPChannelHandler(private val serverMode: Boolean,
if (log.isTraceEnabled) {
withMDC { log.trace("Pipeline uncaught exception", cause) }
}
if (cause is ProxyConnectException) {
log.warn("Proxy connection failed ${cause.message}")
suppressClose = true // The pipeline gets marked as active on connection to the proxy rather than to the target, which causes excess close events
}
ctx.close()
}
@ -176,27 +213,27 @@ internal class AMQPChannelHandler(private val serverMode: Boolean,
try {
try {
when (msg) {
// Transfers application packet into the AMQP engine.
// Transfers application packet into the AMQP engine.
is SendableMessageImpl -> {
val inetAddress = InetSocketAddress(msg.destinationLink.host, msg.destinationLink.port)
logDebugWithMDC { "Message for endpoint $inetAddress , expected $remoteAddress "}
logDebugWithMDC { "Message for endpoint $inetAddress , expected $remoteAddress " }
require(CordaX500Name.parse(msg.destinationLegalName) == CordaX500Name.build(remoteCert!!.subjectX500Principal)) {
"Message for incorrect legal identity ${msg.destinationLegalName} expected ${remoteCert!!.subjectX500Principal}"
}
logDebugWithMDC { "channel write ${msg.applicationProperties["_AMQ_DUPL_ID"]}" }
logDebugWithMDC { "channel write ${msg.applicationProperties[MESSAGE_ID_KEY]}" }
eventProcessor!!.transportWriteMessage(msg)
}
// A received AMQP packet has been completed and this self-posted packet will be signalled out to the
// external application.
// A received AMQP packet has been completed and this self-posted packet will be signalled out to the
// external application.
is ReceivedMessage -> {
onReceive(msg)
}
// A general self-posted event that triggers creation of AMQP frames when required.
// A general self-posted event that triggers creation of AMQP frames when required.
is Transport -> {
eventProcessor!!.transportProcessOutput(ctx)
}
// A self-posted event that forwards status updates for delivered packets to the application.
// A self-posted event that forwards status updates for delivered packets to the application.
is ReceivedMessageImpl.MessageCompleter -> {
eventProcessor!!.complete(msg)
}
@ -210,4 +247,65 @@ internal class AMQPChannelHandler(private val serverMode: Boolean,
}
eventProcessor!!.processEventsAsync()
}
private fun handleSuccessfulHandshake(ctx: ChannelHandlerContext) {
val sslHandler = ctx.pipeline().get(SslHandler::class.java)
val sslSession = sslHandler.engine().session
// Depending on what matching method is used, getting the local certificate is done by selecting the
// appropriate keyManagerFactory
val keyManagerFactory = requestedServerName?.let {
keyManagerFactoriesMap[it]
} ?: keyManagerFactoriesMap.values.single()
localCert = keyManagerFactory.getCurrentCertChain()?.first()
if (localCert == null) {
log.error("SSL KeyManagerFactory failed to provide a local cert")
ctx.close()
return
}
if (sslSession.peerCertificates == null || sslSession.peerCertificates.isEmpty()) {
log.error("No peer certificates")
ctx.close()
return
}
remoteCert = sslHandler.engine().session.peerCertificates.first().x509
val remoteX500Name = try {
CordaX500Name.build(remoteCert!!.subjectX500Principal)
} catch (ex: IllegalArgumentException) {
badCert = true
logErrorWithMDC("Certificate subject not a valid CordaX500Name", ex)
ctx.close()
return
}
if (allowedRemoteLegalNames != null && remoteX500Name !in allowedRemoteLegalNames) {
badCert = true
logErrorWithMDC("Provided certificate subject $remoteX500Name not in expected set $allowedRemoteLegalNames")
ctx.close()
return
}
logInfoWithMDC { "Handshake completed with subject: $remoteX500Name, requested server name: ${sslHandler.getRequestedServerName()}." }
createAMQPEngine(ctx)
onOpen(ctx.channel() as SocketChannel, ConnectionChange(remoteAddress, remoteCert, connected = true, badCert = false))
}
private fun handleFailedHandshake(ctx: ChannelHandlerContext, evt: SslHandshakeCompletionEvent) {
val cause = evt.cause()
// This happens when the peer node is closed during SSL establishment.
when {
cause is ClosedChannelException -> logWarnWithMDC("SSL Handshake closed early.")
// Sadly the exception thrown by Netty wrapper requires that we check the message.
cause is SSLException && cause.message == "handshake timed out" -> logWarnWithMDC("SSL Handshake timed out")
cause is SSLException && (cause.message?.contains("close_notify") == true)
-> logWarnWithMDC("Received close_notify during handshake")
else -> badCert = true
}
logWarnWithMDC("Handshake failure: ${evt.cause().message}")
if (log.isTraceEnabled) {
withMDC { log.trace("Handshake failure", evt.cause()) }
}
ctx.close()
}
}

View File

@ -7,24 +7,45 @@ import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.handler.logging.LogLevel
import io.netty.handler.logging.LoggingHandler
import io.netty.handler.proxy.HttpProxyHandler
import io.netty.handler.proxy.Socks4ProxyHandler
import io.netty.handler.proxy.Socks5ProxyHandler
import io.netty.resolver.NoopAddressResolverGroup
import io.netty.util.internal.logging.InternalLoggerFactory
import io.netty.util.internal.logging.Slf4JLoggerFactory
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
import net.corda.nodeapi.internal.protonwrapper.messages.SendableMessage
import net.corda.nodeapi.internal.protonwrapper.messages.impl.SendableMessageImpl
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPChannelHandler.Companion.PROXY_LOGGER_NAME
import net.corda.nodeapi.internal.requireMessageSize
import rx.Observable
import rx.subjects.PublishSubject
import java.lang.Long.min
import java.net.InetSocketAddress
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.TrustManagerFactory
import kotlin.concurrent.withLock
enum class ProxyVersion {
SOCKS4,
SOCKS5,
HTTP
}
data class ProxyConfig(val version: ProxyVersion, val proxyAddress: NetworkHostAndPort, val userName: String? = null, val password: String? = null, val proxyTimeoutMS: Long? = null) {
init {
if (version == ProxyVersion.SOCKS4) {
require(password == null) { "SOCKS4 does not support a password" }
}
}
}
/**
* The AMQPClient creates a connection initiator that will try to connect in a round-robin fashion
* to the first open SSL socket. It will keep retrying until it is stopped.
@ -42,15 +63,18 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
}
val log = contextLogger()
const val MIN_RETRY_INTERVAL = 1000L
const val MAX_RETRY_INTERVAL = 60000L
const val BACKOFF_MULTIPLIER = 2L
const val NUM_CLIENT_THREADS = 2
private const val CORDA_AMQP_NUM_CLIENT_THREAD_PROP_NAME = "net.corda.nodeapi.amqpclient.NumClientThread"
private const val MIN_RETRY_INTERVAL = 1000L
private const val MAX_RETRY_INTERVAL = 60000L
private const val BACKOFF_MULTIPLIER = 2L
private val NUM_CLIENT_THREADS = Integer.getInteger(CORDA_AMQP_NUM_CLIENT_THREAD_PROP_NAME, 2)
}
private val lock = ReentrantLock()
@Volatile
private var stopping: Boolean = false
private var started: Boolean = false
private var workerGroup: EventLoopGroup? = null
@Volatile
private var clientChannel: Channel? = null
@ -59,6 +83,13 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
private var currentTarget: NetworkHostAndPort = targets.first()
private var retryInterval = MIN_RETRY_INTERVAL
private val badCertTargets = mutableSetOf<NetworkHostAndPort>()
@Volatile
private var amqpActive = false
@Volatile
private var amqpChannelHandler: ChannelHandler? = null
val localAddressString: String
get() = clientChannel?.localAddress()?.toString() ?: "<unknownLocalAddress>"
private fun nextTarget() {
val origIndex = targetIndex
@ -80,29 +111,31 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
private val connectListener = object : ChannelFutureListener {
override fun operationComplete(future: ChannelFuture) {
amqpActive = false
if (!future.isSuccess) {
log.info("Failed to connect to $currentTarget")
if (!stopping) {
if (started) {
workerGroup?.schedule({
nextTarget()
restart()
}, retryInterval, TimeUnit.MILLISECONDS)
}
} else {
log.info("Connected to $currentTarget")
// Connection established successfully
clientChannel = future.channel()
clientChannel?.closeFuture()?.addListener(closeListener)
log.info("Connected to $currentTarget, Local address: $localAddressString")
}
}
}
private val closeListener = ChannelFutureListener { future ->
log.info("Disconnected from $currentTarget")
log.info("Disconnected from $currentTarget, Local address: $localAddressString")
future.channel()?.disconnect()
clientChannel = null
if (!stopping) {
if (started && !amqpActive) {
log.debug { "Scheduling restart of $currentTarget (AMQP inactive)" }
workerGroup?.schedule({
nextTarget()
restart()
@ -114,42 +147,110 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
private val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
private val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
private val conf = parent.configuration
@Volatile
private lateinit var amqpChannelHandler: AMQPChannelHandler
init {
keyManagerFactory.init(conf.keyStore)
trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, conf.crlCheckSoftFail))
trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, conf.revocationConfig))
}
@Suppress("ComplexMethod")
override fun initChannel(ch: SocketChannel) {
val pipeline = ch.pipeline()
val proxyConfig = conf.proxyConfig
if (proxyConfig != null) {
if (conf.trace) pipeline.addLast(PROXY_LOGGER_NAME, LoggingHandler(LogLevel.INFO))
val proxyAddress = InetSocketAddress(proxyConfig.proxyAddress.host, proxyConfig.proxyAddress.port)
val proxy = when (conf.proxyConfig!!.version) {
ProxyVersion.SOCKS4 -> {
Socks4ProxyHandler(proxyAddress, proxyConfig.userName)
}
ProxyVersion.SOCKS5 -> {
Socks5ProxyHandler(proxyAddress, proxyConfig.userName, proxyConfig.password)
}
ProxyVersion.HTTP -> {
val httpProxyHandler = if(proxyConfig.userName == null || proxyConfig.password == null) {
HttpProxyHandler(proxyAddress)
} else {
HttpProxyHandler(proxyAddress, proxyConfig.userName, proxyConfig.password)
}
//httpProxyHandler.setConnectTimeoutMillis(3600000) // 1hr for debugging purposes
httpProxyHandler
}
}
val proxyTimeout = proxyConfig.proxyTimeoutMS
if (proxyTimeout != null) {
proxy.setConnectTimeoutMillis(proxyTimeout)
}
pipeline.addLast("Proxy", proxy)
proxy.connectFuture().addListener {
if (!it.isSuccess) {
ch.disconnect()
}
}
}
val wrappedKeyManagerFactory = CertHoldingKeyManagerFactoryWrapper(keyManagerFactory, parent.configuration)
val target = parent.currentTarget
val handler = createClientSslHelper(target, parent.allowedRemoteLegalNames, keyManagerFactory, trustManagerFactory)
val handler = if (parent.configuration.useOpenSsl) {
createClientOpenSslHandler(target, parent.allowedRemoteLegalNames, wrappedKeyManagerFactory, trustManagerFactory, ch.alloc())
} else {
createClientSslHelper(target, parent.allowedRemoteLegalNames, wrappedKeyManagerFactory, trustManagerFactory)
}
handler.handshakeTimeoutMillis = conf.sslHandshakeTimeout
pipeline.addLast("sslHandler", handler)
if (conf.trace) pipeline.addLast("logger", LoggingHandler(LogLevel.INFO))
pipeline.addLast(AMQPChannelHandler(false,
amqpChannelHandler = AMQPChannelHandler(false,
parent.allowedRemoteLegalNames,
// Single entry, key can be anything.
mapOf(DEFAULT to wrappedKeyManagerFactory),
conf.userName,
conf.password,
conf.trace,
{
parent.retryInterval = MIN_RETRY_INTERVAL // reset to fast reconnect if we connect properly
parent._onConnection.onNext(it.second)
},
{
parent._onConnection.onNext(it.second)
if (it.second.badCert) {
log.error("Blocking future connection attempts to $target due to bad certificate on endpoint")
parent.badCertTargets += target
false,
onOpen = { _, change ->
parent.run {
amqpActive = true
retryInterval = MIN_RETRY_INTERVAL // reset to fast reconnect if we connect properly
_onConnection.onNext(change)
}
},
{ rcv -> parent._onReceive.onNext(rcv) }))
onClose = { _, change ->
if (parent.amqpChannelHandler == amqpChannelHandler) {
parent.run {
_onConnection.onNext(change)
if (change.badCert) {
log.error("Blocking future connection attempts to $target due to bad certificate on endpoint")
badCertTargets += target
}
if (started && amqpActive) {
log.debug { "Scheduling restart of $currentTarget (AMQP active)" }
workerGroup?.schedule({
nextTarget()
restart()
}, retryInterval, TimeUnit.MILLISECONDS)
}
amqpActive = false
}
}
},
onReceive = { rcv -> parent._onReceive.onNext(rcv) })
parent.amqpChannelHandler = amqpChannelHandler
pipeline.addLast(amqpChannelHandler)
}
}
fun start() {
lock.withLock {
log.info("connect to: $currentTarget")
if (started) {
log.info("Already connected to: $currentTarget so returning")
return
}
log.info("Connect to: $currentTarget")
workerGroup = sharedThreadPool ?: NioEventLoopGroup(NUM_CLIENT_THREADS)
started = true
restart()
}
}
@ -161,6 +262,10 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
val bootstrap = Bootstrap()
// TODO Needs more configuration control when we profile. e.g. to use EPOLL on Linux
bootstrap.group(workerGroup).channel(NioSocketChannel::class.java).handler(ClientChannelInitializer(this))
// Delegate DNS Resolution to the proxy side, if we are using proxy.
if (configuration.proxyConfig != null) {
bootstrap.resolver(NoopAddressResolverGroup.INSTANCE)
}
currentTarget = targets[targetIndex]
val clientFuture = bootstrap.connect(currentTarget.host, currentTarget.port)
clientFuture.addListener(connectListener)
@ -168,21 +273,17 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
fun stop() {
lock.withLock {
log.info("disconnect from: $currentTarget")
stopping = true
try {
if (sharedThreadPool == null) {
workerGroup?.shutdownGracefully()
workerGroup?.terminationFuture()?.sync()
} else {
clientChannel?.close()?.sync()
}
clientChannel = null
workerGroup = null
} finally {
stopping = false
log.info("Stopping connection to: $currentTarget, Local address: $localAddressString")
started = false
if (sharedThreadPool == null) {
workerGroup?.shutdownGracefully()
workerGroup?.terminationFuture()?.sync()
} else {
clientChannel?.close()?.sync()
}
log.info("stopped connection to $currentTarget")
clientChannel = null
workerGroup = null
log.info("Stopped connection to $currentTarget")
}
}
@ -191,7 +292,7 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
val connected: Boolean
get() {
val channel = lock.withLock { clientChannel }
return channel?.isActive ?: false
return isChannelWritable(channel)
}
fun createMessage(payload: ByteArray,
@ -204,13 +305,17 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
fun write(msg: SendableMessage) {
val channel = clientChannel
if (channel == null) {
if (channel == null || !isChannelWritable(channel)) {
throw IllegalStateException("Connection to $targets not active")
} else {
channel.writeAndFlush(msg)
}
}
private fun isChannelWritable(channel: Channel?): Boolean {
return channel?.let { channel.isOpen && channel.isActive && amqpActive } ?: false
}
private val _onReceive = PublishSubject.create<ReceivedMessage>().toSerialized()
val onReceive: Observable<ReceivedMessage>
get() = _onReceive

View File

@ -2,7 +2,7 @@ package net.corda.nodeapi.internal.protonwrapper.netty
import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.config.CertificateStore
import java.security.KeyStore
import net.corda.nodeapi.internal.config.DEFAULT_SSL_HANDSHAKE_TIMEOUT_MILLIS
interface AMQPConfiguration {
/**
@ -32,12 +32,11 @@ interface AMQPConfiguration {
val trustStore: CertificateStore
/**
* Setting crlCheckSoftFail to true allows certificate paths where some leaf certificates do not contain cRLDistributionPoints
* and also allows validation to continue if the CRL distribution server is not contactable.
* Control how CRL check will be performed.
*/
@JvmDefault
val crlCheckSoftFail: Boolean
get() = true
val revocationConfig: RevocationConfig
get() = RevocationConfigImpl(RevocationConfig.Mode.SOFT_FAIL)
/**
* Enables full debug tracing of all netty and AMQP level packets. This logs aat very high volume and is only for developers.
@ -51,5 +50,41 @@ interface AMQPConfiguration {
* but currently that is deferred to Artemis and the bridge code.
*/
val maxMessageSize: Int
@JvmDefault
val proxyConfig: ProxyConfig?
get() = null
@JvmDefault
val sourceX500Name: String?
get() = null
/**
* Whether to use the tcnative open/boring SSL provider or the default Java SSL provider
*/
@JvmDefault
val useOpenSsl: Boolean
get() = false
@JvmDefault
val sslHandshakeTimeout: Long
get() = DEFAULT_SSL_HANDSHAKE_TIMEOUT_MILLIS // Aligned with sun.security.provider.certpath.URICertStore.DEFAULT_CRL_CONNECT_TIMEOUT
/**
* An optional Health Check Phrase which if passed through the channel will cause AMQP Server to echo it back instead of doing normal pipeline processing
*/
val healthCheckPhrase: String?
get() = null
/**
* An optional set of IPv4/IPv6 remote address strings which will be compared to the remote address of inbound connections and these will only log at TRACE level
*/
@JvmDefault
val silencedIPs: Set<String>
get() = emptySet()
@JvmDefault
val enableSNI: Boolean
get() = true
}

View File

@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.protonwrapper.netty
import io.netty.bootstrap.ServerBootstrap
import io.netty.channel.Channel
import io.netty.channel.ChannelHandler
import io.netty.channel.ChannelInitializer
import io.netty.channel.ChannelOption
import io.netty.channel.EventLoopGroup
@ -14,6 +15,7 @@ import io.netty.util.internal.logging.InternalLoggerFactory
import io.netty.util.internal.logging.Slf4JLoggerFactory
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
import net.corda.nodeapi.internal.protonwrapper.messages.SendableMessage
import net.corda.nodeapi.internal.protonwrapper.messages.impl.SendableMessageImpl
@ -31,7 +33,6 @@ import kotlin.concurrent.withLock
/**
* This create a socket acceptor instance that can receive possibly multiple AMQP connections.
* As of now this is not used outside of testing, but in future it will be used for standalone bridging components.
*/
class AMQPServer(val hostName: String,
val port: Int,
@ -42,8 +43,10 @@ class AMQPServer(val hostName: String,
InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE)
}
private const val CORDA_AMQP_NUM_SERVER_THREAD_PROP_NAME = "net.corda.nodeapi.amqpserver.NumServerThreads"
private val log = contextLogger()
const val NUM_SERVER_THREADS = 4
private val NUM_SERVER_THREADS = Integer.getInteger(CORDA_AMQP_NUM_SERVER_THREAD_PROP_NAME, 4)
}
private val lock = ReentrantLock()
@ -60,29 +63,59 @@ class AMQPServer(val hostName: String,
private val conf = parent.configuration
init {
keyManagerFactory.init(conf.keyStore)
trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, conf.crlCheckSoftFail))
keyManagerFactory.init(conf.keyStore.value.internal, conf.keyStore.entryPassword.toCharArray())
trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, conf.revocationConfig))
}
override fun initChannel(ch: SocketChannel) {
val amqpConfiguration = parent.configuration
val pipeline = ch.pipeline()
val handler = createServerSslHelper(keyManagerFactory, trustManagerFactory)
pipeline.addLast("sslHandler", handler)
amqpConfiguration.healthCheckPhrase?.let { pipeline.addLast(ModeSelectingChannel.NAME, ModeSelectingChannel(it)) }
val (sslHandler, keyManagerFactoriesMap) = createSSLHandler(amqpConfiguration, ch)
pipeline.addLast("sslHandler", sslHandler)
if (conf.trace) pipeline.addLast("logger", LoggingHandler(LogLevel.INFO))
val suppressLogs = ch.remoteAddress()?.hostString in amqpConfiguration.silencedIPs
pipeline.addLast(AMQPChannelHandler(true,
null,
// Passing a mapping of legal names to key managers to be able to pick the correct one after
// SNI completion event is fired up.
keyManagerFactoriesMap,
conf.userName,
conf.password,
conf.trace,
{
parent.clientChannels[it.first.remoteAddress()] = it.first
parent._onConnection.onNext(it.second)
suppressLogs,
onOpen = { channel, change ->
parent.run {
clientChannels[channel.remoteAddress()] = channel
_onConnection.onNext(change)
}
},
{
parent.clientChannels.remove(it.first.remoteAddress())
parent._onConnection.onNext(it.second)
onClose = { channel, change ->
parent.run {
val remoteAddress = channel.remoteAddress()
clientChannels.remove(remoteAddress)
_onConnection.onNext(change)
}
},
{ rcv -> parent._onReceive.onNext(rcv) }))
onReceive = { rcv -> parent._onReceive.onNext(rcv) }))
}
private fun createSSLHandler(amqpConfig: AMQPConfiguration, ch: SocketChannel): Pair<ChannelHandler, Map<String, CertHoldingKeyManagerFactoryWrapper>> {
return if (amqpConfig.useOpenSsl && amqpConfig.enableSNI && amqpConfig.keyStore.aliases().size > 1) {
val keyManagerFactoriesMap = splitKeystore(amqpConfig)
// SNI matching needed only when multiple nodes exist behind the server.
Pair(createServerSNIOpenSslHandler(keyManagerFactoriesMap, trustManagerFactory), keyManagerFactoriesMap)
} else {
val keyManagerFactory = CertHoldingKeyManagerFactoryWrapper(keyManagerFactory, amqpConfig)
val handler = if (amqpConfig.useOpenSsl) {
createServerOpenSslHandler(keyManagerFactory, trustManagerFactory, ch.alloc())
} else {
// For javaSSL, SNI matching is handled at key manager level.
createServerSslHandler(amqpConfig.keyStore, keyManagerFactory, trustManagerFactory)
}
handler.handshakeTimeoutMillis = amqpConfig.sslHandshakeTimeout
Pair(handler, mapOf(DEFAULT to keyManagerFactory))
}
}
}
@ -95,7 +128,10 @@ class AMQPServer(val hostName: String,
val server = ServerBootstrap()
// TODO Needs more configuration control when we profile. e.g. to use EPOLL on Linux
server.group(bossGroup, workerGroup).channel(NioServerSocketChannel::class.java).option(ChannelOption.SO_BACKLOG, 100).handler(LoggingHandler(LogLevel.INFO)).childHandler(ServerChannelInitializer(this))
server.group(bossGroup, workerGroup).channel(NioServerSocketChannel::class.java)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(NettyServerEventLogger(LogLevel.INFO, configuration.silencedIPs))
.childHandler(ServerChannelInitializer(this))
log.info("Try to bind $port")
val channelFuture = server.bind(hostName, port).sync() // block/throw here as better to know we failed to claim port than carry on
@ -144,7 +180,7 @@ class AMQPServer(val hostName: String,
requireMessageSize(payload.size, configuration.maxMessageSize)
val dest = InetSocketAddress(destinationLink.host, destinationLink.port)
require(dest in clientChannels.keys) {
"Destination not available"
"Destination $dest is not available"
}
return SendableMessageImpl(payload, topic, destinationLegalName, destinationLink, properties)
}
@ -155,21 +191,22 @@ class AMQPServer(val hostName: String,
if (channel == null) {
throw IllegalStateException("Connection to ${msg.destinationLink} not active")
} else {
log.debug { "Writing message with payload of size ${msg.payload.size} into channel $channel" }
channel.writeAndFlush(msg)
log.debug { "Done writing message with payload of size ${msg.payload.size} into channel $channel" }
}
}
fun dropConnection(connectionRemoteHost: InetSocketAddress) {
val channel = clientChannels[connectionRemoteHost]
if (channel != null) {
channel.close()
}
clientChannels[connectionRemoteHost]?.close()
}
fun complete(delivery: Delivery, target: InetSocketAddress) {
val channel = clientChannels[target]
channel?.apply {
log.debug { "Writing delivery $delivery into channel $channel" }
writeAndFlush(delivery)
log.debug { "Done writing delivery $delivery into channel $channel" }
}
}

View File

@ -0,0 +1,60 @@
package net.corda.nodeapi.internal.protonwrapper.netty
import java.net.Socket
import java.security.Principal
import javax.net.ssl.SSLEngine
import javax.net.ssl.X509ExtendedKeyManager
import javax.net.ssl.X509KeyManager
interface AliasProvidingKeyMangerWrapper : X509KeyManager {
var lastAlias: String?
}
class AliasProvidingKeyMangerWrapperImpl(private val keyManager: X509KeyManager) : AliasProvidingKeyMangerWrapper, X509KeyManager by keyManager {
override var lastAlias: String? = null
override fun chooseServerAlias(keyType: String?, issuers: Array<out Principal>?, socket: Socket?): String? {
return storeIfNotNull { keyManager.chooseServerAlias(keyType, issuers, socket) }
}
override fun chooseClientAlias(keyType: Array<out String>?, issuers: Array<out Principal>?, socket: Socket?): String? {
return storeIfNotNull { keyManager.chooseClientAlias(keyType, issuers, socket) }
}
private fun storeIfNotNull(func: () -> String?): String? {
val alias = func()
if (alias != null) {
lastAlias = alias
}
return alias
}
}
class AliasProvidingExtendedKeyMangerWrapper(private val keyManager: X509ExtendedKeyManager) : X509ExtendedKeyManager(), X509KeyManager by keyManager, AliasProvidingKeyMangerWrapper {
override var lastAlias: String? = null
override fun chooseServerAlias(keyType: String?, issuers: Array<out Principal>?, socket: Socket?): String? {
return storeIfNotNull { keyManager.chooseServerAlias(keyType, issuers, socket) }
}
override fun chooseClientAlias(keyType: Array<out String>?, issuers: Array<out Principal>?, socket: Socket?): String? {
return storeIfNotNull { keyManager.chooseClientAlias(keyType, issuers, socket) }
}
override fun chooseEngineClientAlias(keyType: Array<out String>?, issuers: Array<out Principal>?, engine: SSLEngine?): String? {
return storeIfNotNull { keyManager.chooseEngineClientAlias(keyType, issuers, engine) }
}
override fun chooseEngineServerAlias(keyType: String?, issuers: Array<out Principal>?, engine: SSLEngine?): String? {
return storeIfNotNull { keyManager.chooseEngineServerAlias(keyType, issuers, engine) }
}
private fun storeIfNotNull(func: () -> String?): String? {
val alias = func()
if (alias != null) {
lastAlias = alias
}
return alias
}
}

View File

@ -0,0 +1,34 @@
package net.corda.nodeapi.internal.protonwrapper.netty
import net.corda.core.utilities.debug
import org.slf4j.LoggerFactory
import java.security.cert.CertPathValidatorException
import java.security.cert.Certificate
import java.security.cert.PKIXRevocationChecker
import java.util.*
object AllowAllRevocationChecker : PKIXRevocationChecker() {
private val logger = LoggerFactory.getLogger(AllowAllRevocationChecker::class.java)
override fun check(cert: Certificate?, unresolvedCritExts: MutableCollection<String>?) {
logger.debug {"Passing certificate check for: $cert"}
// Nothing to do
}
override fun isForwardCheckingSupported(): Boolean {
return true
}
override fun getSupportedExtensions(): MutableSet<String>? {
return null
}
override fun init(forward: Boolean) {
// Nothing to do
}
override fun getSoftFailExceptions(): MutableList<CertPathValidatorException> {
return LinkedList()
}
}

View File

@ -0,0 +1,81 @@
package net.corda.nodeapi.internal.protonwrapper.netty
import java.security.KeyStore
import java.security.cert.X509Certificate
import javax.net.ssl.KeyManager
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.KeyManagerFactorySpi
import javax.net.ssl.ManagerFactoryParameters
import javax.net.ssl.X509ExtendedKeyManager
import javax.net.ssl.X509KeyManager
class CertHoldingKeyManagerFactorySpiWrapper(private val factorySpi: KeyManagerFactorySpi, private val amqpConfig: AMQPConfiguration) : KeyManagerFactorySpi() {
override fun engineInit(keyStore: KeyStore?, password: CharArray?) {
val engineInitMethod = KeyManagerFactorySpi::class.java.getDeclaredMethod("engineInit", KeyStore::class.java, CharArray::class.java)
engineInitMethod.isAccessible = true
engineInitMethod.invoke(factorySpi, keyStore, password)
}
override fun engineInit(spec: ManagerFactoryParameters?) {
val engineInitMethod = KeyManagerFactorySpi::class.java.getDeclaredMethod("engineInit", ManagerFactoryParameters::class.java)
engineInitMethod.isAccessible = true
engineInitMethod.invoke(factorySpi, spec)
}
private fun getKeyManagersImpl(): Array<KeyManager> {
val engineGetKeyManagersMethod = KeyManagerFactorySpi::class.java.getDeclaredMethod("engineGetKeyManagers")
engineGetKeyManagersMethod.isAccessible = true
@Suppress("UNCHECKED_CAST")
val keyManagers = engineGetKeyManagersMethod.invoke(factorySpi) as Array<KeyManager>
return if (factorySpi is CertHoldingKeyManagerFactorySpiWrapper) keyManagers else keyManagers.map {
val aliasProvidingKeyManager = getDefaultKeyManager(it)
// Use the SNIKeyManager if keystore has several entries and only for clients and non-openSSL servers.
// Condition of using SNIKeyManager: if its client, or JDKSsl server.
val isClient = amqpConfig.sourceX500Name != null
val enableSNI = amqpConfig.enableSNI && amqpConfig.keyStore.aliases().size > 1
if (enableSNI && (isClient || !amqpConfig.useOpenSsl)) {
SNIKeyManager(aliasProvidingKeyManager as X509ExtendedKeyManager, amqpConfig)
} else {
aliasProvidingKeyManager
}
}.toTypedArray()
}
private fun getDefaultKeyManager(keyManager: KeyManager): KeyManager {
return when (keyManager) {
is X509ExtendedKeyManager -> AliasProvidingExtendedKeyMangerWrapper(keyManager)
is X509KeyManager -> AliasProvidingKeyMangerWrapperImpl(keyManager)
else -> throw UnsupportedOperationException("Supported key manager types are: X509ExtendedKeyManager, X509KeyManager. Provided ${keyManager::class.java.name}")
}
}
private val keyManagers = lazy { getKeyManagersImpl() }
override fun engineGetKeyManagers(): Array<KeyManager> {
return keyManagers.value
}
}
/**
* You can wrap a key manager factory in this class if you need to get the cert chain currently used to identify or
* verify. When using for TLS channels, make sure to wrap the (singleton) factory separately on each channel, as
* the wrapper is not thread safe as in it will return the last used alias/cert chain and has itself no notion
* of belonging to a certain channel.
*/
class CertHoldingKeyManagerFactoryWrapper(factory: KeyManagerFactory, amqpConfig: AMQPConfiguration) : KeyManagerFactory(getFactorySpi(factory, amqpConfig), factory.provider, factory.algorithm) {
companion object {
private fun getFactorySpi(factory: KeyManagerFactory, amqpConfig: AMQPConfiguration): KeyManagerFactorySpi {
val spiField = KeyManagerFactory::class.java.getDeclaredField("factorySpi")
spiField.isAccessible = true
return CertHoldingKeyManagerFactorySpiWrapper(spiField.get(factory) as KeyManagerFactorySpi, amqpConfig)
}
}
fun getCurrentCertChain(): Array<out X509Certificate>? {
val keyManager = keyManagers.firstOrNull()
val alias = if (keyManager is AliasProvidingKeyMangerWrapper) keyManager.lastAlias else null
return if (alias != null && keyManager is X509KeyManager) {
keyManager.getCertificateChain(alias)
} else null
}
}

View File

@ -0,0 +1,12 @@
package net.corda.nodeapi.internal.protonwrapper.netty
import java.security.cert.X509CRL
import java.security.cert.X509Certificate
interface ExternalCrlSource {
/**
* Given certificate provides a set of CRLs, potentially performing remote communication.
*/
fun fetch(certificate: X509Certificate) : Set<X509CRL>
}

View File

@ -0,0 +1,76 @@
package net.corda.nodeapi.internal.protonwrapper.netty
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import io.netty.channel.ChannelHandlerContext
import io.netty.handler.codec.ByteToMessageDecoder
import io.netty.handler.ssl.SslHandler
import net.corda.core.utilities.contextLogger
/**
* Responsible for deciding whether we are likely to be processing health probe request
* or this is a normal SSL/AMQP processing pipeline
*/
internal class ModeSelectingChannel(healthCheckPhrase: String) : ByteToMessageDecoder() {
companion object {
const val NAME = "modeSelector"
private val log = contextLogger()
}
private enum class TriState {
UNDECIDED,
ECHO_MODE,
NORMAL_MODE
}
private val healthCheckPhraseArray = healthCheckPhrase.toByteArray(Charsets.UTF_8)
private var currentMode = TriState.UNDECIDED
private var alreadyEchoedPos = 0
override fun decode(ctx: ChannelHandlerContext, inByteBuf: ByteBuf, out: MutableList<Any>?) {
fun ChannelHandlerContext.echoBack(inByteBuf: ByteBuf) {
// WriteAndFlush() will decrement count and will blow unless we retain first
// And we have to ensure we are not sending the same information multiple times
val toBeWritten = inByteBuf.retainedSlice(alreadyEchoedPos, inByteBuf.readableBytes() - alreadyEchoedPos)
writeAndFlush(toBeWritten)
alreadyEchoedPos = inByteBuf.readableBytes()
}
if(currentMode == TriState.ECHO_MODE) {
ctx.echoBack(inByteBuf)
return
}
// Wait until the length prefix is available.
if (inByteBuf.readableBytes() < healthCheckPhraseArray.size) {
return
}
// Direct buffers do not allow calling `.array()` on them, see `io.netty.buffer.UnpooledDirectByteBuf.array`
val incomingArray = Unpooled.copiedBuffer(inByteBuf).array()
val zipped = healthCheckPhraseArray.zip(incomingArray)
if (zipped.all { it.first == it.second }) {
// Matched the healthCheckPhrase
currentMode = TriState.ECHO_MODE
log.info("Echo mode activated for connection ${ctx.channel().id()}")
// Cancel scheduled action to avoid SSL handshake timeout, which starts "ticking" upon connection is established,
// namely upon call to `io.netty.handler.ssl.SslHandler#handlerAdded` is made
ctx.pipeline().get(SslHandler::class.java)?.handshakeFuture()?.cancel(false)
ctx.echoBack(inByteBuf)
} else {
currentMode = TriState.NORMAL_MODE
// Remove self from pipeline and replay all the messages received down the pipeline
// It is important to bump-up reference count as pipeline removal decrements it by one.
inByteBuf.retain()
ctx.pipeline().remove(this)
ctx.fireChannelRead(inByteBuf)
}
}
}

View File

@ -0,0 +1,73 @@
package net.corda.nodeapi.internal.protonwrapper.netty
import io.netty.channel.ChannelDuplexHandler
import io.netty.channel.ChannelHandler
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelPromise
import io.netty.handler.logging.LogLevel
import io.netty.util.internal.logging.InternalLogLevel
import io.netty.util.internal.logging.InternalLogger
import io.netty.util.internal.logging.InternalLoggerFactory
import java.net.SocketAddress
@ChannelHandler.Sharable
class NettyServerEventLogger(level: LogLevel = DEFAULT_LEVEL, val silencedIPs: Set<String> = emptySet()) : ChannelDuplexHandler() {
companion object {
val DEFAULT_LEVEL: LogLevel = LogLevel.DEBUG
}
private val logger: InternalLogger = InternalLoggerFactory.getInstance(javaClass)
private val internalLevel: InternalLogLevel = level.toInternalLevel()
@Throws(Exception::class)
override fun channelActive(ctx: ChannelHandlerContext) {
if (logger.isEnabled(internalLevel)) {
logger.log(internalLevel, "Server socket ${ctx.channel()} ACTIVE")
}
ctx.fireChannelActive()
}
@Throws(Exception::class)
override fun channelInactive(ctx: ChannelHandlerContext) {
if (logger.isEnabled(internalLevel)) {
logger.log(internalLevel, "Server socket ${ctx.channel()} INACTIVE")
}
ctx.fireChannelInactive()
}
@Suppress("OverridingDeprecatedMember")
@Throws(Exception::class)
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
if (logger.isEnabled(internalLevel)) {
logger.log(internalLevel, "Server socket ${ctx.channel()} EXCEPTION ${cause.message}", cause)
}
ctx.fireExceptionCaught(cause)
}
@Throws(Exception::class)
override fun bind(ctx: ChannelHandlerContext, localAddress: SocketAddress, promise: ChannelPromise) {
if (logger.isEnabled(internalLevel)) {
logger.log(internalLevel, "Server socket ${ctx.channel()} BIND $localAddress")
}
ctx.bind(localAddress, promise)
}
@Throws(Exception::class)
override fun close(ctx: ChannelHandlerContext, promise: ChannelPromise) {
if (logger.isEnabled(internalLevel)) {
logger.log(internalLevel, "Server socket ${ctx.channel()} CLOSE")
}
ctx.close(promise)
}
@Throws(Exception::class)
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
val level = if (msg is io.netty.channel.socket.SocketChannel) { // Should always be the case as this is a server socket, but be defensive
if (msg.remoteAddress()?.hostString !in silencedIPs) internalLevel else InternalLogLevel.TRACE
} else internalLevel
if (logger.isEnabled(level)) {
logger.log(level, "Server socket ${ctx.channel()} ACCEPTED $msg")
}
ctx.fireChannelRead(msg)
}
}

View File

@ -0,0 +1,83 @@
package net.corda.nodeapi.internal.protonwrapper.netty
import com.typesafe.config.Config
import net.corda.nodeapi.internal.config.ConfigParser
import net.corda.nodeapi.internal.config.CustomConfigParser
/**
* Data structure for controlling the way how Certificate Revocation Lists are handled.
*/
@CustomConfigParser(RevocationConfigParser::class)
interface RevocationConfig {
enum class Mode {
/**
* @see java.security.cert.PKIXRevocationChecker.Option.SOFT_FAIL
*/
SOFT_FAIL,
/**
* Opposite of SOFT_FAIL - i.e. most rigorous check.
* Among other things, this check requires that CRL checking URL is available on every level of certificate chain.
* This is also known as Strict mode.
*/
HARD_FAIL,
/**
* CRLs are obtained from external source
* @see ExternalCrlSource
*/
EXTERNAL_SOURCE,
/**
* Switch CRL check off.
*/
OFF
}
val mode: Mode
/**
* Optional `ExternalCrlSource` which only makes sense with `mode` = `EXTERNAL_SOURCE`
*/
val externalCrlSource: ExternalCrlSource?
/**
* Creates a copy of `RevocationConfig` with ExternalCrlSource enriched
*/
fun enrichExternalCrlSource(sourceFunc: (() -> ExternalCrlSource)?): RevocationConfig
}
/**
* Maintained for legacy purposes to convert old style `crlCheckSoftFail`.
*/
fun Boolean.toRevocationConfig() = if(this) RevocationConfigImpl(RevocationConfig.Mode.SOFT_FAIL) else RevocationConfigImpl(RevocationConfig.Mode.HARD_FAIL)
data class RevocationConfigImpl(override val mode: RevocationConfig.Mode, override val externalCrlSource: ExternalCrlSource? = null) : RevocationConfig {
override fun enrichExternalCrlSource(sourceFunc: (() -> ExternalCrlSource)?): RevocationConfig {
return if(mode != RevocationConfig.Mode.EXTERNAL_SOURCE) {
this
} else {
assert(sourceFunc != null) { "There should be a way to obtain ExternalCrlSource" }
copy(externalCrlSource = sourceFunc!!())
}
}
}
class RevocationConfigParser : ConfigParser<RevocationConfig> {
override fun parse(config: Config): RevocationConfig {
val oneAndTheOnly = "mode"
val allKeys = config.entrySet().map { it.key }
require(allKeys.size == 1 && allKeys.contains(oneAndTheOnly)) {"For RevocationConfig, it is expected to have '$oneAndTheOnly' property only. " +
"Actual set of properties: $allKeys. Please check 'revocationConfig' section."}
val mode = config.getString(oneAndTheOnly)
return when (mode.toUpperCase()) {
"SOFT_FAIL" -> RevocationConfigImpl(RevocationConfig.Mode.SOFT_FAIL)
"HARD_FAIL" -> RevocationConfigImpl(RevocationConfig.Mode.HARD_FAIL)
"EXTERNAL_SOURCE" -> RevocationConfigImpl(RevocationConfig.Mode.EXTERNAL_SOURCE, null) // null for now till `enrichExternalCrlSource` is called
"OFF" -> RevocationConfigImpl(RevocationConfig.Mode.OFF)
else -> throw IllegalArgumentException("Unsupported mode : '$mode'")
}
}
}

View File

@ -0,0 +1,112 @@
package net.corda.nodeapi.internal.protonwrapper.netty
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.crypto.x509
import org.slf4j.MDC
import java.net.Socket
import java.security.Principal
import javax.net.ssl.SNIMatcher
import javax.net.ssl.SSLEngine
import javax.net.ssl.SSLSocket
import javax.net.ssl.X509ExtendedKeyManager
import javax.net.ssl.X509KeyManager
internal class SNIKeyManager(private val keyManager: X509ExtendedKeyManager, private val amqpConfig: AMQPConfiguration) : X509ExtendedKeyManager(), X509KeyManager by keyManager, AliasProvidingKeyMangerWrapper {
companion object {
private val log = contextLogger()
}
override var lastAlias: String? = null
private fun withMDC(block: () -> Unit) {
val oldMDC = MDC.getCopyOfContextMap()
try {
MDC.put("lastAlias", lastAlias)
MDC.put("isServer", amqpConfig.sourceX500Name.isNullOrEmpty().toString())
MDC.put("sourceX500Name", amqpConfig.sourceX500Name)
MDC.put("useOpenSSL", amqpConfig.useOpenSsl.toString())
block()
} finally {
MDC.setContextMap(oldMDC)
}
}
private fun logDebugWithMDC(msg: () -> String) {
if (log.isDebugEnabled) {
withMDC { log.debug(msg()) }
}
}
override fun chooseClientAlias(keyType: Array<out String>, issuers: Array<out Principal>, socket: Socket): String? {
return storeIfNotNull { chooseClientAlias(amqpConfig.keyStore, amqpConfig.sourceX500Name) }
}
override fun chooseEngineClientAlias(keyType: Array<out String>, issuers: Array<out Principal>, engine: SSLEngine): String? {
return storeIfNotNull { chooseClientAlias(amqpConfig.keyStore, amqpConfig.sourceX500Name) }
}
override fun chooseServerAlias(keyType: String?, issuers: Array<out Principal>?, socket: Socket): String? {
return storeIfNotNull {
val matcher = (socket as SSLSocket).sslParameters.sniMatchers.first()
chooseServerAlias(keyType, issuers, matcher)
}
}
override fun chooseEngineServerAlias(keyType: String?, issuers: Array<out Principal>?, engine: SSLEngine?): String? {
return storeIfNotNull {
val matcher = engine?.sslParameters?.sniMatchers?.first()
chooseServerAlias(keyType, issuers, matcher)
}
}
private fun chooseServerAlias(keyType: String?, issuers: Array<out Principal>?, matcher: SNIMatcher?): String? {
val aliases = keyManager.getServerAliases(keyType, issuers)
if (aliases == null || aliases.isEmpty()) {
logDebugWithMDC { "Keystore doesn't contain any aliases for key type $keyType and issuers $issuers." }
return null
}
log.debug("Checking aliases: $aliases.")
matcher?.let {
val matchedAlias = (it as ServerSNIMatcher).matchedAlias
if (aliases.contains(matchedAlias)) {
logDebugWithMDC { "Found match for $matchedAlias." }
return matchedAlias
}
}
logDebugWithMDC { "Unable to find a matching alias." }
return null
}
private fun chooseClientAlias(keyStore: CertificateStore, clientLegalName: String?): String? {
clientLegalName?.let {
val aliases = keyStore.aliases()
if (aliases.isEmpty()) {
logDebugWithMDC { "Keystore doesn't contain any entries." }
}
aliases.forEach { alias ->
val x500Name = keyStore[alias].x509.subjectX500Principal
val aliasCordaX500Name = CordaX500Name.build(x500Name)
val clientCordaX500Name = CordaX500Name.parse(it)
if (clientCordaX500Name == aliasCordaX500Name) {
logDebugWithMDC { "Found alias $alias for $clientCordaX500Name." }
return alias
}
}
}
return null
}
private fun storeIfNotNull(func: () -> String?): String? {
val alias = func()
if (alias != null) {
lastAlias = alias
}
return alias
}
}

View File

@ -1,28 +1,91 @@
package net.corda.nodeapi.internal.protonwrapper.netty
import io.netty.buffer.ByteBufAllocator
import io.netty.handler.ssl.ClientAuth
import io.netty.handler.ssl.SniHandler
import io.netty.handler.ssl.SslContextBuilder
import io.netty.handler.ssl.SslHandler
import io.netty.handler.ssl.SslProvider
import io.netty.util.DomainNameMappingBuilder
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.newSecureRandom
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.VisibleForTesting
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.toHex
import net.corda.nodeapi.internal.ArtemisTcpTransport
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.crypto.toBc
import net.corda.nodeapi.internal.crypto.x509
import net.corda.nodeapi.internal.protonwrapper.netty.revocation.ExternalSourceRevocationChecker
import org.bouncycastle.asn1.ASN1InputStream
import org.bouncycastle.asn1.DERIA5String
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier
import org.bouncycastle.asn1.x509.CRLDistPoint
import org.bouncycastle.asn1.x509.DistributionPointName
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralNames
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier
import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
import java.net.Socket
import java.security.KeyStore
import java.security.cert.*
import java.util.*
import java.util.concurrent.Executor
import javax.net.ssl.*
import kotlin.system.measureTimeMillis
private const val HOSTNAME_FORMAT = "%s.corda.net"
private const val SSL_HANDSHAKE_TIMEOUT_PROP_NAME = "corda.netty.sslHelper.handshakeTimeout"
private const val DEFAULT_SSL_TIMEOUT = 20000 // Aligned with sun.security.provider.certpath.URICertStore.DEFAULT_CRL_CONNECT_TIMEOUT
internal const val DEFAULT = "default"
internal class LoggingTrustManagerWrapper(val wrapped: X509ExtendedTrustManager) : X509ExtendedTrustManager() {
internal const val DP_DEFAULT_ANSWER = "NO CRLDP ext"
internal val logger = LoggerFactory.getLogger("net.corda.nodeapi.internal.protonwrapper.netty.SSLHelper")
fun X509Certificate.distributionPoints() : Set<String>? {
logger.debug("Checking CRLDPs for $subjectX500Principal")
val crldpExtBytes = getExtensionValue(Extension.cRLDistributionPoints.id)
if (crldpExtBytes == null) {
logger.debug(DP_DEFAULT_ANSWER)
return emptySet()
}
val derObjCrlDP = ASN1InputStream(ByteArrayInputStream(crldpExtBytes)).readObject()
val dosCrlDP = derObjCrlDP as? DEROctetString
if (dosCrlDP == null) {
logger.error("Expected to have DEROctetString, actual type: ${derObjCrlDP.javaClass}")
return emptySet()
}
val crldpExtOctetsBytes = dosCrlDP.octets
val dpObj = ASN1InputStream(ByteArrayInputStream(crldpExtOctetsBytes)).readObject()
val distPoint = CRLDistPoint.getInstance(dpObj)
if (distPoint == null) {
logger.error("Could not instantiate CRLDistPoint, from: $dpObj")
return emptySet()
}
val dpNames = distPoint.distributionPoints.mapNotNull { it.distributionPoint }.filter { it.type == DistributionPointName.FULL_NAME }
val generalNames = dpNames.flatMap { GeneralNames.getInstance(it.name).names.asList() }
return generalNames.filter { it.tagNo == GeneralName.uniformResourceIdentifier}.map { DERIA5String.getInstance(it.name).string }.toSet()
}
fun X509Certificate.distributionPointsToString() : String {
return with(distributionPoints()) {
if(this == null || isEmpty()) {
DP_DEFAULT_ANSWER
} else {
sorted().joinToString()
}
}
}
@VisibleForTesting
class LoggingTrustManagerWrapper(val wrapped: X509ExtendedTrustManager) : X509ExtendedTrustManager() {
companion object {
val log = contextLogger()
}
@ -45,12 +108,11 @@ internal class LoggingTrustManagerWrapper(val wrapped: X509ExtendedTrustManager)
} catch (ex: Exception) {
"null"
}
" $subject[$keyIdentifier] issued by $issuer[$authorityKeyIdentifier]"
" $subject[$keyIdentifier] issued by $issuer[$authorityKeyIdentifier] [${it.distributionPointsToString()}]"
}
return certs.joinToString("\r\n")
}
private fun certPathToStringFull(chain: Array<out X509Certificate>?): String {
if (chain == null) {
return "<empty certpath>"
@ -107,6 +169,33 @@ internal class LoggingTrustManagerWrapper(val wrapped: X509ExtendedTrustManager)
}
private object LoggingImmediateExecutor : Executor {
override fun execute(command: Runnable?) {
val log = LoggerFactory.getLogger(javaClass)
if (command == null) {
log.error("SSL handler executor called with a null command")
throw NullPointerException("command")
}
@Suppress("TooGenericExceptionCaught", "MagicNumber") // log and rethrow all exceptions
try {
val commandName = command::class.qualifiedName?.let { "[$it]" } ?: ""
log.debug("Entering SSL command $commandName")
val elapsedTime = measureTimeMillis { command.run() }
log.debug("Exiting SSL command $elapsedTime millis")
if (elapsedTime > 100) {
log.info("Command: $commandName took $elapsedTime millis to execute")
}
}
catch (ex: Exception) {
log.error("Caught exception in SSL handler executor", ex)
throw ex
}
}
}
internal fun createClientSslHelper(target: NetworkHostAndPort,
expectedRemoteLegalNames: Set<CordaX500Name>,
keyManagerFactory: KeyManagerFactory,
@ -125,13 +214,31 @@ internal fun createClientSslHelper(target: NetworkHostAndPort,
sslParameters.serverNames = listOf(SNIHostName(x500toHostName(expectedRemoteLegalNames.single())))
sslEngine.sslParameters = sslParameters
}
val sslHandler = SslHandler(sslEngine)
sslHandler.handshakeTimeoutMillis = Integer.getInteger(SSL_HANDSHAKE_TIMEOUT_PROP_NAME, DEFAULT_SSL_TIMEOUT).toLong()
return sslHandler
@Suppress("DEPRECATION")
return SslHandler(sslEngine, false, LoggingImmediateExecutor)
}
internal fun createServerSslHelper(keyManagerFactory: KeyManagerFactory,
trustManagerFactory: TrustManagerFactory): SslHandler {
internal fun createClientOpenSslHandler(target: NetworkHostAndPort,
expectedRemoteLegalNames: Set<CordaX500Name>,
keyManagerFactory: KeyManagerFactory,
trustManagerFactory: TrustManagerFactory,
alloc: ByteBufAllocator): SslHandler {
val sslContext = SslContextBuilder.forClient().sslProvider(SslProvider.OPENSSL).keyManager(keyManagerFactory).trustManager(LoggingTrustManagerFactoryWrapper(trustManagerFactory)).build()
val sslEngine = sslContext.newEngine(alloc, target.host, target.port)
sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray()
sslEngine.enabledCipherSuites = ArtemisTcpTransport.CIPHER_SUITES.toTypedArray()
if (expectedRemoteLegalNames.size == 1) {
val sslParameters = sslEngine.sslParameters
sslParameters.serverNames = listOf(SNIHostName(x500toHostName(expectedRemoteLegalNames.single())))
sslEngine.sslParameters = sslParameters
}
@Suppress("DEPRECATION")
return SslHandler(sslEngine, false, LoggingImmediateExecutor)
}
internal fun createServerSslHandler(keyStore: CertificateStore,
keyManagerFactory: KeyManagerFactory,
trustManagerFactory: TrustManagerFactory): SslHandler {
val sslContext = SSLContext.getInstance("TLS")
val keyManagers = keyManagerFactory.keyManagers
val trustManagers = trustManagerFactory.trustManagers.filterIsInstance(X509ExtendedTrustManager::class.java).map { LoggingTrustManagerWrapper(it) }.toTypedArray()
@ -142,35 +249,106 @@ internal fun createServerSslHelper(keyManagerFactory: KeyManagerFactory,
sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray()
sslEngine.enabledCipherSuites = ArtemisTcpTransport.CIPHER_SUITES.toTypedArray()
sslEngine.enableSessionCreation = true
val sslHandler = SslHandler(sslEngine)
sslHandler.handshakeTimeoutMillis = Integer.getInteger(SSL_HANDSHAKE_TIMEOUT_PROP_NAME, DEFAULT_SSL_TIMEOUT).toLong()
return sslHandler
val sslParameters = sslEngine.sslParameters
sslParameters.sniMatchers = listOf(ServerSNIMatcher(keyStore))
sslEngine.sslParameters = sslParameters
@Suppress("DEPRECATION")
return SslHandler(sslEngine, false, LoggingImmediateExecutor)
}
internal fun initialiseTrustStoreAndEnableCrlChecking(trustStore: CertificateStore, crlCheckSoftFail: Boolean): ManagerFactoryParameters {
val certPathBuilder = CertPathBuilder.getInstance("PKIX")
val revocationChecker = certPathBuilder.revocationChecker as PKIXRevocationChecker
revocationChecker.options = EnumSet.of(
// Prefer CRL over OCSP
PKIXRevocationChecker.Option.PREFER_CRLS,
// Don't fall back to OCSP checking
PKIXRevocationChecker.Option.NO_FALLBACK)
if (crlCheckSoftFail) {
// Allow revocation check to succeed if the revocation status cannot be determined for one of
// the following reasons: The CRL or OCSP response cannot be obtained because of a network error.
revocationChecker.options = revocationChecker.options + PKIXRevocationChecker.Option.SOFT_FAIL
}
@VisibleForTesting
fun initialiseTrustStoreAndEnableCrlChecking(trustStore: CertificateStore, revocationConfig: RevocationConfig): ManagerFactoryParameters {
val pkixParams = PKIXBuilderParameters(trustStore.value.internal, X509CertSelector())
val revocationChecker = when (revocationConfig.mode) {
RevocationConfig.Mode.OFF -> AllowAllRevocationChecker // Custom PKIXRevocationChecker skipping CRL check
RevocationConfig.Mode.EXTERNAL_SOURCE -> {
require(revocationConfig.externalCrlSource != null) { "externalCrlSource must not be null" }
ExternalSourceRevocationChecker(revocationConfig.externalCrlSource!!) { Date() } // Custom PKIXRevocationChecker which uses `externalCrlSource`
}
else -> {
val certPathBuilder = CertPathBuilder.getInstance("PKIX")
val pkixRevocationChecker = certPathBuilder.revocationChecker as PKIXRevocationChecker
pkixRevocationChecker.options = EnumSet.of(
// Prefer CRL over OCSP
PKIXRevocationChecker.Option.PREFER_CRLS,
// Don't fall back to OCSP checking
PKIXRevocationChecker.Option.NO_FALLBACK)
if (revocationConfig.mode == RevocationConfig.Mode.SOFT_FAIL) {
// Allow revocation check to succeed if the revocation status cannot be determined for one of
// the following reasons: The CRL or OCSP response cannot be obtained because of a network error.
pkixRevocationChecker.options = pkixRevocationChecker.options + PKIXRevocationChecker.Option.SOFT_FAIL
}
pkixRevocationChecker
}
}
pkixParams.addCertPathChecker(revocationChecker)
return CertPathTrustManagerParameters(pkixParams)
}
internal fun createServerOpenSslHandler(keyManagerFactory: KeyManagerFactory,
trustManagerFactory: TrustManagerFactory,
alloc: ByteBufAllocator): SslHandler {
val sslContext = getServerSslContextBuilder(keyManagerFactory, trustManagerFactory).build()
val sslEngine = sslContext.newEngine(alloc)
sslEngine.useClientMode = false
@Suppress("DEPRECATION")
return SslHandler(sslEngine, false, LoggingImmediateExecutor)
}
/**
* Creates a special SNI handler used only when openSSL is used for AMQPServer
*/
internal fun createServerSNIOpenSslHandler(keyManagerFactoriesMap: Map<String, KeyManagerFactory>,
trustManagerFactory: TrustManagerFactory): SniHandler {
// Default value can be any in the map.
val sslCtxBuilder = getServerSslContextBuilder(keyManagerFactoriesMap.values.first(), trustManagerFactory)
val mapping = DomainNameMappingBuilder(sslCtxBuilder.build())
keyManagerFactoriesMap.forEach {
mapping.add(it.key, sslCtxBuilder.keyManager(it.value).build())
}
return SniHandler(mapping.build())
}
@Suppress("SpreadOperator")
private fun getServerSslContextBuilder(keyManagerFactory: KeyManagerFactory, trustManagerFactory: TrustManagerFactory): SslContextBuilder {
return SslContextBuilder.forServer(keyManagerFactory)
.sslProvider(SslProvider.OPENSSL)
.trustManager(LoggingTrustManagerFactoryWrapper(trustManagerFactory))
.clientAuth(ClientAuth.REQUIRE)
.ciphers(ArtemisTcpTransport.CIPHER_SUITES)
.protocols(*ArtemisTcpTransport.TLS_VERSIONS.toTypedArray())
}
internal fun splitKeystore(config: AMQPConfiguration): Map<String, CertHoldingKeyManagerFactoryWrapper> {
val keyStore = config.keyStore.value.internal
val password = config.keyStore.entryPassword.toCharArray()
return keyStore.aliases().toList().map { alias ->
val key = keyStore.getKey(alias, password)
val certs = keyStore.getCertificateChain(alias)
val x500Name = keyStore.getCertificate(alias).x509.subjectX500Principal
val cordaX500Name = CordaX500Name.build(x500Name)
val newKeyStore = KeyStore.getInstance("JKS")
newKeyStore.load(null)
newKeyStore.setKeyEntry(alias, key, password, certs)
val newKeyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
newKeyManagerFactory.init(newKeyStore, password)
x500toHostName(cordaX500Name) to CertHoldingKeyManagerFactoryWrapper(newKeyManagerFactory, config)
}.toMap()
}
// As per Javadoc in: https://docs.oracle.com/javase/8/docs/api/javax/net/ssl/KeyManagerFactory.html `init` method
// 2nd parameter `password` - the password for recovering keys in the KeyStore
fun KeyManagerFactory.init(keyStore: CertificateStore) = init(keyStore.value.internal, keyStore.entryPassword.toCharArray())
fun TrustManagerFactory.init(trustStore: CertificateStore) = init(trustStore.value.internal)
/**
* Method that converts a [CordaX500Name] to a a valid hostname (RFC-1035). It's used for SNI to indicate the target
* when trying to communicate with nodes that reside behind the same firewall. This is a solution to TLS's extension not
* yet supporting x500 names as server names
*/
internal fun x500toHostName(x500Name: CordaX500Name): String {
val secureHash = SecureHash.sha256(x500Name.toString())
// RFC 1035 specifies a limit 255 bytes for hostnames with each label being 63 bytes or less. Due to this, the string

View File

@ -0,0 +1,47 @@
package net.corda.nodeapi.internal.protonwrapper.netty
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.crypto.x509
import javax.net.ssl.SNIHostName
import javax.net.ssl.SNIMatcher
import javax.net.ssl.SNIServerName
import javax.net.ssl.StandardConstants
class ServerSNIMatcher(private val keyStore: CertificateStore) : SNIMatcher(0) {
companion object {
val log = contextLogger()
}
var matchedAlias: String? = null
private set
var matchedServerName: String? = null
private set
override fun matches(serverName: SNIServerName): Boolean {
if (serverName.type == StandardConstants.SNI_HOST_NAME) {
keyStore.aliases().forEach { alias ->
val x500Name = keyStore[alias].x509.subjectX500Principal
val cordaX500Name = CordaX500Name.build(x500Name)
// Convert the CordaX500Name into the expected host name and compare
// E.g. O=Corda B, L=London, C=GB becomes 3c6dd991936308edb210555103ffc1bb.corda.net
if ((serverName as SNIHostName).asciiName == x500toHostName(cordaX500Name)) {
matchedAlias = alias
matchedServerName = serverName.asciiName
return true
}
}
}
val knownSNIValues = keyStore.aliases().joinToString {
val x500Name = keyStore[it].x509.subjectX500Principal
val cordaX500Name = CordaX500Name.build(x500Name)
"hostname = ${x500toHostName(cordaX500Name)} alias = $it"
}
val requestedSNIValue = "hostname = ${(serverName as SNIHostName).asciiName}"
log.warn("The requested SNI value [$requestedSNIValue] does not match any of the following known SNI values [$knownSNIValues]")
return false
}
}

View File

@ -0,0 +1,40 @@
package net.corda.nodeapi.internal.protonwrapper.netty
import java.security.KeyStore
import javax.net.ssl.ManagerFactoryParameters
import javax.net.ssl.TrustManager
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.TrustManagerFactorySpi
import javax.net.ssl.X509ExtendedTrustManager
class LoggingTrustManagerFactorySpiWrapper(private val factorySpi: TrustManagerFactorySpi) : TrustManagerFactorySpi() {
override fun engineGetTrustManagers(): Array<TrustManager> {
val engineGetTrustManagersMethod = TrustManagerFactorySpi::class.java.getDeclaredMethod("engineGetTrustManagers")
engineGetTrustManagersMethod.isAccessible = true
@Suppress("UNCHECKED_CAST")
val trustManagers = engineGetTrustManagersMethod.invoke(factorySpi) as Array<TrustManager>
return if (factorySpi is LoggingTrustManagerFactorySpiWrapper) trustManagers else trustManagers.filterIsInstance(X509ExtendedTrustManager::class.java).map { LoggingTrustManagerWrapper(it) }.toTypedArray()
}
override fun engineInit(ks: KeyStore?) {
val engineInitMethod = TrustManagerFactorySpi::class.java.getDeclaredMethod("engineInit", KeyStore::class.java)
engineInitMethod.isAccessible = true
engineInitMethod.invoke(factorySpi, ks)
}
override fun engineInit(spec: ManagerFactoryParameters?) {
val engineInitMethod = TrustManagerFactorySpi::class.java.getDeclaredMethod("engineInit", ManagerFactoryParameters::class.java)
engineInitMethod.isAccessible = true
engineInitMethod.invoke(factorySpi, spec)
}
}
class LoggingTrustManagerFactoryWrapper(factory: TrustManagerFactory) : TrustManagerFactory(getFactorySpi(factory), factory.provider, factory.algorithm) {
companion object {
private fun getFactorySpi(factory: TrustManagerFactory): TrustManagerFactorySpi {
val spiField = TrustManagerFactory::class.java.getDeclaredField("factorySpi")
spiField.isAccessible = true
return LoggingTrustManagerFactorySpiWrapper(spiField.get(factory) as TrustManagerFactorySpi)
}
}
}

View File

@ -0,0 +1,88 @@
package net.corda.nodeapi.internal.protonwrapper.netty.revocation
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.protonwrapper.netty.ExternalCrlSource
import org.bouncycastle.asn1.x509.Extension
import java.security.cert.CRLReason
import java.security.cert.CertPathValidatorException
import java.security.cert.Certificate
import java.security.cert.CertificateRevokedException
import java.security.cert.PKIXRevocationChecker
import java.security.cert.X509CRL
import java.security.cert.X509Certificate
import java.util.*
/**
* Implementation of [PKIXRevocationChecker] which determines whether certificate is revoked using [externalCrlSource] which knows how to
* obtain a set of CRLs for a given certificate from an external source
*/
class ExternalSourceRevocationChecker(private val externalCrlSource: ExternalCrlSource, private val dateSource: () -> Date) : PKIXRevocationChecker() {
companion object {
private val logger = contextLogger()
}
override fun check(cert: Certificate, unresolvedCritExts: MutableCollection<String>?) {
val x509Certificate = cert as X509Certificate
checkApprovedCRLs(x509Certificate, externalCrlSource.fetch(x509Certificate))
}
/**
* Borrowed from `RevocationChecker.checkApprovedCRLs()`
*/
@Suppress("NestedBlockDepth")
@Throws(CertPathValidatorException::class)
private fun checkApprovedCRLs(cert: X509Certificate, approvedCRLs: Set<X509CRL>) {
// See if the cert is in the set of approved crls.
logger.debug("ExternalSourceRevocationChecker.checkApprovedCRLs() cert SN: ${cert.serialNumber}")
for (crl in approvedCRLs) {
val entry = crl.getRevokedCertificate(cert)
if (entry != null) {
logger.debug("ExternalSourceRevocationChecker.checkApprovedCRLs() CRL entry: $entry")
/*
* Abort CRL validation and throw exception if there are any
* unrecognized critical CRL entry extensions (see section
* 5.3 of RFC 5280).
*/
val unresCritExts = entry.criticalExtensionOIDs
if (unresCritExts != null && !unresCritExts.isEmpty()) {
/* remove any that we will process */
unresCritExts.remove(Extension.cRLDistributionPoints.id)
unresCritExts.remove(Extension.certificateIssuer.id)
if (!unresCritExts.isEmpty()) {
throw CertPathValidatorException(
"Unrecognized critical extension(s) in revoked CRL entry: $unresCritExts")
}
}
val reasonCode = entry.revocationReason ?: CRLReason.UNSPECIFIED
val revocationDate = entry.revocationDate
if (revocationDate.before(dateSource())) {
val t = CertificateRevokedException(
revocationDate, reasonCode,
crl.issuerX500Principal, mutableMapOf())
throw CertPathValidatorException(
t.message, t, null, -1, CertPathValidatorException.BasicReason.REVOKED)
}
}
}
}
override fun isForwardCheckingSupported(): Boolean {
return true
}
override fun getSupportedExtensions(): MutableSet<String>? {
return null
}
override fun init(forward: Boolean) {
// Nothing to do
}
override fun getSoftFailExceptions(): MutableList<CertPathValidatorException> {
return LinkedList()
}
}

View File

@ -0,0 +1,121 @@
package net.corda.nodeapi.internal.serialization.kryo
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import java.lang.reflect.Constructor
import java.lang.reflect.Field
import java.util.LinkedList
/**
* The [LinkedHashMap] and [LinkedHashSet] have a problem with the default Quasar/Kryo serialisation
* in that serialising an iterator (and subsequent [LinkedHashMap.Entry]) over a sufficiently large
* data set can lead to a stack overflow (because the object map is traversed recursively).
*
* We've added our own custom serializer in order to ensure that the iterator is correctly deserialized.
*/
internal object LinkedHashMapIteratorSerializer : Serializer<Iterator<*>>() {
private val DUMMY_MAP = linkedMapOf(1L to 1)
private val outerMapField: Field = getIterator()::class.java.superclass.getDeclaredField("this$0").apply { isAccessible = true }
private val currentField: Field = getIterator()::class.java.superclass.getDeclaredField("current").apply { isAccessible = true }
private val KEY_ITERATOR_CLASS: Class<MutableIterator<Long>> = DUMMY_MAP.keys.iterator().javaClass
private val VALUE_ITERATOR_CLASS: Class<MutableIterator<Int>> = DUMMY_MAP.values.iterator().javaClass
private val MAP_ITERATOR_CLASS: Class<MutableIterator<MutableMap.MutableEntry<Long, Int>>> = DUMMY_MAP.iterator().javaClass
fun getIterator(): Any = DUMMY_MAP.iterator()
override fun write(kryo: Kryo, output: Output, obj: Iterator<*>) {
val current: Map.Entry<*, *>? = currentField.get(obj) as Map.Entry<*, *>?
kryo.writeClassAndObject(output, outerMapField.get(obj))
kryo.writeClassAndObject(output, current)
}
override fun read(kryo: Kryo, input: Input, type: Class<Iterator<*>>): Iterator<*> {
val outerMap = kryo.readClassAndObject(input) as Map<*, *>
return when (type) {
KEY_ITERATOR_CLASS -> {
val current = (kryo.readClassAndObject(input) as? Map.Entry<*, *>)?.key
outerMap.keys.iterator().returnToIteratorLocation(current)
}
VALUE_ITERATOR_CLASS -> {
val current = (kryo.readClassAndObject(input) as? Map.Entry<*, *>)?.value
outerMap.values.iterator().returnToIteratorLocation(current)
}
MAP_ITERATOR_CLASS -> {
val current = (kryo.readClassAndObject(input) as? Map.Entry<*, *>)
outerMap.iterator().returnToIteratorLocation(current)
}
else -> throw IllegalStateException("Invalid type")
}
}
private fun Iterator<*>.returnToIteratorLocation(current: Any?) : Iterator<*> {
while (this.hasNext()) {
val key = this.next()
@Suppress("SuspiciousEqualsCombination")
if (current == null || key === current || key == current) {
break
}
}
return this
}
}
/**
* The [LinkedHashMap] and [LinkedHashSet] have a problem with the default Quasar/Kryo serialisation
* in that serialising an iterator (and subsequent [LinkedHashMap.Entry]) over a sufficiently large
* data set can lead to a stack overflow (because the object map is traversed recursively).
*
* We've added our own custom serializer in order to ensure that only the key/value are recorded.
* The rest of the list isn't required at this scope.
*/
object LinkedHashMapEntrySerializer : Serializer<Map.Entry<*, *>>() {
// Create a dummy map so that we can get the LinkedHashMap$Entry from it
// The element type of the map doesn't matter. The entry is all we want
private val DUMMY_MAP = linkedMapOf(1L to 1)
fun getEntry(): Any = DUMMY_MAP.entries.first()
private val constr: Constructor<*> = getEntry()::class.java.declaredConstructors.single().apply { isAccessible = true }
/**
* Kryo would end up serialising "this" entry, then serialise "this.after" recursively, leading to a very large stack.
* we'll skip that and just write out the key/value
*/
override fun write(kryo: Kryo, output: Output, obj: Map.Entry<*, *>) {
val e: Map.Entry<*, *> = obj
kryo.writeClassAndObject(output, e.key)
kryo.writeClassAndObject(output, e.value)
}
override fun read(kryo: Kryo, input: Input, type: Class<Map.Entry<*, *>>): Map.Entry<*, *> {
val key = kryo.readClassAndObject(input)
val value = kryo.readClassAndObject(input)
return constr.newInstance(0, key, value, null) as Map.Entry<*, *>
}
}
/**
* Also, add a [ListIterator] serializer to avoid more linked list issues.
*/
object LinkedListItrSerializer : Serializer<ListIterator<Any>>() {
// Create a dummy list so that we can get the ListItr from it
// The element type of the list doesn't matter. The iterator is all we want
private val DUMMY_LIST = LinkedList<Long>(listOf(1))
fun getListItr(): Any = DUMMY_LIST.listIterator()
private val outerListField: Field = getListItr()::class.java.getDeclaredField("this$0").apply { isAccessible = true }
override fun write(kryo: Kryo, output: Output, obj: ListIterator<Any>) {
kryo.writeClassAndObject(output, outerListField.get(obj))
output.writeInt(obj.nextIndex())
}
override fun read(kryo: Kryo, input: Input, type: Class<ListIterator<Any>>): ListIterator<Any> {
val list = kryo.readClassAndObject(input) as LinkedList<*>
val index = input.readInt()
return list.listIterator(index)
}
}

View File

@ -10,7 +10,11 @@ import com.esotericsoftware.kryo.serializers.FieldSerializer
import de.javakaffee.kryoserializers.ArraysAsListSerializer
import de.javakaffee.kryoserializers.BitSetSerializer
import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer
import de.javakaffee.kryoserializers.guava.*
import de.javakaffee.kryoserializers.guava.ImmutableListSerializer
import de.javakaffee.kryoserializers.guava.ImmutableMapSerializer
import de.javakaffee.kryoserializers.guava.ImmutableMultimapSerializer
import de.javakaffee.kryoserializers.guava.ImmutableSetSerializer
import de.javakaffee.kryoserializers.guava.ImmutableSortedSetSerializer
import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractClassName
import net.corda.core.contracts.PrivacySalt
@ -24,7 +28,11 @@ import net.corda.core.serialization.MissingAttachmentsException
import net.corda.core.serialization.SerializationWhitelist
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SerializedBytes
import net.corda.core.transactions.*
import net.corda.core.transactions.ContractUpgradeFilteredTransaction
import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.toNonEmptySet
import net.corda.serialization.internal.DefaultWhitelist
@ -51,8 +59,9 @@ import java.security.PrivateKey
import java.security.PublicKey
import java.security.cert.CertPath
import java.security.cert.X509Certificate
import java.util.*
import kotlin.collections.ArrayList
import java.util.Arrays
import java.util.BitSet
import java.util.ServiceLoader
object DefaultKryoCustomizer {
private val serializationWhitelists: List<SerializationWhitelist> by lazy {
@ -70,7 +79,8 @@ object DefaultKryoCustomizer {
instantiatorStrategy = CustomInstantiatorStrategy()
// Required for HashCheckingStream (de)serialization.
// Note that return type should be specifically set to InputStream, otherwise it may not work, i.e. val aStream : InputStream = HashCheckingStream(...).
// Note that return type should be specifically set to InputStream, otherwise it may not work,
// i.e. val aStream : InputStream = HashCheckingStream(...).
addDefaultSerializer(InputStream::class.java, InputStreamSerializer)
addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer<SerializeAsToken>())
addDefaultSerializer(Logger::class.java, LoggerSerializer)
@ -79,8 +89,10 @@ object DefaultKryoCustomizer {
// WARNING: reordering the registrations here will cause a change in the serialized form, since classes
// with custom serializers get written as registration ids. This will break backwards-compatibility.
// Please add any new registrations to the end.
// TODO: re-organise registrations into logical groups before v1.0
addDefaultSerializer(LinkedHashMapIteratorSerializer.getIterator()::class.java.superclass, LinkedHashMapIteratorSerializer)
register(LinkedHashMapEntrySerializer.getEntry()::class.java, LinkedHashMapEntrySerializer)
register(LinkedListItrSerializer.getListItr()::class.java, LinkedListItrSerializer)
register(Arrays.asList("").javaClass, ArraysAsListSerializer())
register(LazyMappedList::class.java, LazyMappedListSerializer)
register(SignedTransaction::class.java, SignedTransactionSerializer)

View File

@ -18,6 +18,8 @@ import net.corda.core.serialization.SerializeAsTokenContext
import net.corda.core.serialization.SerializedBytes
import net.corda.core.transactions.*
import net.corda.core.utilities.OpaqueBytes
import net.corda.serialization.internal.checkUseCase
import net.corda.core.utilities.SgxSupport
import net.corda.serialization.internal.serializationContextKey
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@ -67,13 +69,17 @@ object SerializedBytesSerializer : Serializer<SerializedBytes<Any>>() {
* set via the constructor and the class is immutable.
*/
class ImmutableClassSerializer<T : Any>(val klass: KClass<T>) : Serializer<T>() {
val props = klass.memberProperties.sortedBy { it.name }
val propsByName = props.associateBy { it.name }
val constructor = klass.primaryConstructor!!
val props by lazy { klass.memberProperties.sortedBy { it.name } }
val propsByName by lazy { props.associateBy { it.name } }
val constructor by lazy { klass.primaryConstructor!! }
init {
props.forEach {
require(it !is KMutableProperty<*>) { "$it mutable property of class: ${klass} is unsupported" }
// Verify that this class is immutable (all properties are final).
// We disable this check inside SGX as the reflection blows up.
if (!SgxSupport.isInsideEnclave) {
props.forEach {
require(it !is KMutableProperty<*>) { "$it mutable property of class: ${klass} is unsupported" }
}
}
}

View File

@ -10,11 +10,20 @@ import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.pool.KryoPool
import com.esotericsoftware.kryo.serializers.ClosureSerializer
import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.*
import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.internal.CheckpointSerializationContext
import net.corda.core.serialization.internal.CheckpointSerializer
import net.corda.core.utilities.ByteSequence
import net.corda.serialization.internal.*
import net.corda.serialization.internal.AlwaysAcceptEncodingWhitelist
import net.corda.serialization.internal.ByteBufferInputStream
import net.corda.serialization.internal.CheckpointSerializationContextImpl
import net.corda.serialization.internal.CordaSerializationEncoding
import net.corda.serialization.internal.CordaSerializationMagic
import net.corda.serialization.internal.QuasarWhitelist
import net.corda.serialization.internal.SectionId
import net.corda.serialization.internal.encodingNotPermittedFormat
import java.util.concurrent.ConcurrentHashMap
val kryoMagic = CordaSerializationMagic("corda".toByteArray() + byteArrayOf(0, 0))

View File

@ -9,7 +9,6 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.div
import net.corda.core.utilities.NetworkHostAndPort
import org.assertj.core.api.Assertions.*
import org.hibernate.exception.DataException
import org.junit.Test
import java.net.URL
import java.nio.file.Path

View File

@ -4,7 +4,10 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.coretesting.internal.configureTestSSL
import net.corda.nodeapi.internal.DEV_CA_KEY_STORE_PASS
import net.corda.nodeapi.internal.DEV_CA_PRIVATE_KEY_PASS
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS
import org.junit.Test
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SNIHostName
@ -23,7 +26,7 @@ class SSLHelperTest {
val keyStore = sslConfig.keyStore
keyManagerFactory.init(CertificateStore.fromFile(keyStore.path, keyStore.storePassword, keyStore.entryPassword, false))
val trustStore = sslConfig.trustStore
trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(CertificateStore.fromFile(trustStore.path, trustStore.storePassword, trustStore.entryPassword, false), false))
trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(CertificateStore.fromFile(trustStore.path, trustStore.storePassword, trustStore.entryPassword, false), RevocationConfigImpl(RevocationConfig.Mode.HARD_FAIL)))
val sslHandler = createClientSslHelper(NetworkHostAndPort("localhost", 1234), setOf(legalName), keyManagerFactory, trustManagerFactory)
val legalNameHash = SecureHash.sha256(legalName.toString()).toString().take(32).toLowerCase()
@ -34,4 +37,14 @@ class SSLHelperTest {
assertEquals(1, sslHandler.engine().sslParameters.serverNames.size)
assertEquals("$legalNameHash.corda.net", (sslHandler.engine().sslParameters.serverNames.first() as SNIHostName).asciiName)
}
@Test(timeout=300_000)
fun `test distributionPointsToString`() {
val certStore = CertificateStore.fromResource(
"net/corda/nodeapi/internal/protonwrapper/netty/sslkeystore_Revoked.jks",
DEV_CA_KEY_STORE_PASS, DEV_CA_PRIVATE_KEY_PASS)
val distPoints = certStore.query { getCertificateChain(CORDA_CLIENT_TLS).map { it.distributionPointsToString() } }
assertEquals(listOf("NO CRLDP ext", "http://day-v3-doorman.cordaconnect.io/doorman",
"http://day3-doorman.cordaconnect.io/doorman", "http://day3-doorman.cordaconnect.io/subordinate", "NO CRLDP ext"), distPoints)
}
}

View File

@ -0,0 +1,56 @@
package net.corda.nodeapi.internal.protonwrapper.netty.revocation
import net.corda.core.utilities.Try
import net.corda.nodeapi.internal.DEV_CA_KEY_STORE_PASS
import net.corda.nodeapi.internal.DEV_CA_PRIVATE_KEY_PASS
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.protonwrapper.netty.ExternalCrlSource
import org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory
import org.junit.Test
import java.math.BigInteger
import java.security.cert.X509CRL
import java.security.cert.X509Certificate
import java.sql.Date
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class ExternalSourceRevocationCheckerTest {
@Test(timeout=300_000)
fun checkRevoked() {
val checkResult = performCheckOnDate(Date.valueOf("2019-09-27"))
val failedChecks = checkResult.filterNot { it.second.isSuccess }
assertEquals(1, failedChecks.size)
assertEquals(BigInteger.valueOf(8310484079152632582), failedChecks.first().first.serialNumber)
}
@Test(timeout=300_000)
fun checkTooEarly() {
val checkResult = performCheckOnDate(Date.valueOf("2019-08-27"))
assertTrue(checkResult.all { it.second.isSuccess })
}
private fun performCheckOnDate(date: Date): List<Pair<X509Certificate, Try<Unit>>> {
val certStore = CertificateStore.fromResource(
"net/corda/nodeapi/internal/protonwrapper/netty/sslkeystore_Revoked.jks",
DEV_CA_KEY_STORE_PASS, DEV_CA_PRIVATE_KEY_PASS)
val resourceAsStream = javaClass.getResourceAsStream("/net/corda/nodeapi/internal/protonwrapper/netty/doorman.crl")
val crl = CertificateFactory().engineGenerateCRL(resourceAsStream) as X509CRL
//val crlHolder = X509CRLHolder(resourceAsStream)
//crlHolder.revokedCertificates as X509CRLEntryHolder
val instance = ExternalSourceRevocationChecker(object : ExternalCrlSource {
override fun fetch(certificate: X509Certificate): Set<X509CRL> = setOf(crl)
}) { date }
return certStore.query {
getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).map {
Pair(it, Try.on { instance.check(it, mutableListOf()) })
}
}
}
}

View File

@ -0,0 +1,132 @@
package net.corda.nodeapi.internal.serialization.kryo
import org.junit.Test
import org.junit.jupiter.api.assertDoesNotThrow
import java.util.LinkedList
import kotlin.test.assertEquals
class KryoCheckpointTest {
private val testSize = 1000L
/**
* This test just ensures that the checkpoints still work in light of [LinkedHashMapEntrySerializer].
*/
@Test(timeout=300_000)
fun `linked hash map can checkpoint without error`() {
var lastKey = ""
val dummyMap = linkedMapOf<String, Long>()
for (i in 0..testSize) {
dummyMap[i.toString()] = i
}
var it = dummyMap.iterator()
while (it.hasNext()) {
lastKey = it.next().key
val bytes = KryoCheckpointSerializer.serialize(it, KRYO_CHECKPOINT_CONTEXT)
it = KryoCheckpointSerializer.deserialize(bytes, it.javaClass, KRYO_CHECKPOINT_CONTEXT)
}
assertEquals(testSize.toString(), lastKey)
}
@Test(timeout=300_000)
fun `empty linked hash map can checkpoint without error`() {
val dummyMap = linkedMapOf<String, Long>()
val it = dummyMap.iterator()
val itKeys = dummyMap.keys.iterator()
val itValues = dummyMap.values.iterator()
val bytes = KryoCheckpointSerializer.serialize(it, KRYO_CHECKPOINT_CONTEXT)
val bytesKeys = KryoCheckpointSerializer.serialize(itKeys, KRYO_CHECKPOINT_CONTEXT)
val bytesValues = KryoCheckpointSerializer.serialize(itValues, KRYO_CHECKPOINT_CONTEXT)
assertDoesNotThrow {
KryoCheckpointSerializer.deserialize(bytes, it.javaClass, KRYO_CHECKPOINT_CONTEXT)
KryoCheckpointSerializer.deserialize(bytesKeys, itKeys.javaClass, KRYO_CHECKPOINT_CONTEXT)
KryoCheckpointSerializer.deserialize(bytesValues, itValues.javaClass, KRYO_CHECKPOINT_CONTEXT)
}
}
@Test(timeout=300_000)
fun `linked hash map with null values can checkpoint without error`() {
val dummyMap = linkedMapOf<String?, Long?>().apply {
put(null, null)
}
val it = dummyMap.iterator()
val bytes = KryoCheckpointSerializer.serialize(it, KRYO_CHECKPOINT_CONTEXT)
val itKeys = dummyMap.keys.iterator()
val bytesKeys = KryoCheckpointSerializer.serialize(itKeys, KRYO_CHECKPOINT_CONTEXT)
val itValues = dummyMap.values.iterator()
val bytesValues = KryoCheckpointSerializer.serialize(itValues, KRYO_CHECKPOINT_CONTEXT)
assertDoesNotThrow {
KryoCheckpointSerializer.deserialize(bytes, it.javaClass, KRYO_CHECKPOINT_CONTEXT)
KryoCheckpointSerializer.deserialize(bytesKeys, itKeys.javaClass, KRYO_CHECKPOINT_CONTEXT)
KryoCheckpointSerializer.deserialize(bytesValues, itValues.javaClass, KRYO_CHECKPOINT_CONTEXT)
}
}
@Test(timeout=300_000)
fun `linked hash map keys can checkpoint without error`() {
var lastKey = ""
val dummyMap = linkedMapOf<String, Long>()
for (i in 0..testSize) {
dummyMap[i.toString()] = i
}
var it = dummyMap.keys.iterator()
while (it.hasNext()) {
lastKey = it.next()
val bytes = KryoCheckpointSerializer.serialize(it, KRYO_CHECKPOINT_CONTEXT)
it = KryoCheckpointSerializer.deserialize(bytes, it.javaClass, KRYO_CHECKPOINT_CONTEXT)
}
assertEquals(testSize.toString(), lastKey)
}
@Test(timeout=300_000)
fun `linked hash map values can checkpoint without error`() {
var lastValue = 0L
val dummyMap = linkedMapOf<String, Long>()
for (i in 0..testSize) {
dummyMap[i.toString()] = i
}
var it = dummyMap.values.iterator()
while (it.hasNext()) {
lastValue = it.next()
val bytes = KryoCheckpointSerializer.serialize(it, KRYO_CHECKPOINT_CONTEXT)
it = KryoCheckpointSerializer.deserialize(bytes, it.javaClass, KRYO_CHECKPOINT_CONTEXT)
}
assertEquals(testSize, lastValue)
}
/**
* This test just ensures that the checkpoints still work in light of [LinkedHashMapEntrySerializer].
*/
@Test(timeout=300_000)
fun `linked hash set can checkpoint without error`() {
var result: Any = 0L
val dummySet = linkedSetOf<Any>().apply { addAll(0..testSize) }
var it = dummySet.iterator()
while (it.hasNext()) {
result = it.next()
val bytes = KryoCheckpointSerializer.serialize(it, KRYO_CHECKPOINT_CONTEXT)
it = KryoCheckpointSerializer.deserialize(bytes, it.javaClass, KRYO_CHECKPOINT_CONTEXT)
}
assertEquals(testSize, result)
}
/**
* This test just ensures that the checkpoints still work in light of [LinkedListItrSerializer].
*/
@Test(timeout=300_000)
fun `linked list can checkpoint without error`() {
var result: Any = 0L
val dummyList = LinkedList<Long>().apply { addAll(0..testSize) }
var it = dummyList.iterator()
while (it.hasNext()) {
result = it.next()
val bytes = KryoCheckpointSerializer.serialize(it, KRYO_CHECKPOINT_CONTEXT)
it = KryoCheckpointSerializer.deserialize(bytes, it.javaClass, KRYO_CHECKPOINT_CONTEXT)
}
assertEquals(testSize, result)
}
}

View File

@ -0,0 +1,3 @@
Represents some test data which contains real certificates produced by DayWatch Doorman as well as CRL list file.
For all the keystores the password is "cordacadevpass".

View File

@ -270,7 +270,8 @@ task slowIntegrationTest(type: Test) {
// quasar exclusions upon agent code instrumentation at run-time
quasar {
excludeClassLoaders.addAll(
'net.corda.djvm.**'
'net.corda.djvm.**',
'net.corda.core.serialization.internal.**'
)
excludePackages.addAll(
"antlr**",

View File

@ -92,7 +92,7 @@ task buildCordaJAR(type: FatCapsule, dependsOn: [
applicationId = "net.corda.node.Corda"
// See experimental/quasar-hook/README.md for how to generate.
def quasarExcludeExpression = "x(antlr**;bftsmart**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;kotlin**;net.corda.djvm**;djvm**;net.bytebuddy**;net.i2p**;org.apache**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**;com.lmax**;picocli**;liquibase**;com.github.benmanes**;org.json**;org.postgresql**;nonapi.io.github.classgraph**)"
def quasarClassLoaderExclusion = "l(net.corda.djvm.**)"
def quasarClassLoaderExclusion = "l(net.corda.djvm.**;net.corda.core.serialization.internal.**)"
javaAgents = quasar_classifier == null ? ["quasar-core-${quasar_version}.jar=${quasarExcludeExpression}${quasarClassLoaderExclusion}"] : ["quasar-core-${quasar_version}-${quasar_classifier}.jar=${quasarExcludeExpression}${quasarClassLoaderExclusion}"]
systemProperties['visualvm.display.name'] = 'Corda'
if (JavaVersion.current() == JavaVersion.VERSION_1_8) {

View File

@ -34,15 +34,15 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() {
val rules = """
RULE Create Counter
CLASS ${ActionExecutorImpl::class.java.name}
METHOD executeSendInitial
METHOD executeSendMultiple
AT ENTRY
IF createCounter("counter", $counter)
DO traceln("Counter created")
ENDRULE
RULE Throw exception on executeSendInitial action
RULE Throw exception on executeSendMultiple action
CLASS ${ActionExecutorImpl::class.java.name}
METHOD executeSendInitial
METHOD executeSendMultiple
AT ENTRY
IF readCounter("counter") < 4
DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die")
@ -114,15 +114,15 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() {
val rules = """
RULE Create Counter
CLASS ${ActionExecutorImpl::class.java.name}
METHOD executeSendInitial
METHOD executeSendMultiple
AT ENTRY
IF createCounter("counter", $counter)
DO traceln("Counter created")
ENDRULE
RULE Throw exception on executeSendInitial action
RULE Throw exception on executeSendMultiple action
CLASS ${ActionExecutorImpl::class.java.name}
METHOD executeSendInitial
METHOD executeSendMultiple
AT ENTRY
IF readCounter("counter") < 3
DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die")

View File

@ -208,15 +208,15 @@ class StatemachineKillFlowErrorHandlingTest : StatemachineErrorHandlingTest() {
val rules = """
RULE Create Counter
CLASS ${ActionExecutorImpl::class.java.name}
METHOD executeSendInitial
METHOD executeSendMultiple
AT ENTRY
IF createCounter("counter", $counter)
DO traceln("Counter created")
ENDRULE
RULE Throw exception on executeSendInitial action
RULE Throw exception on executeSendMultiple action
CLASS ${ActionExecutorImpl::class.java.name}
METHOD executeSendInitial
METHOD executeSendMultiple
AT ENTRY
IF readCounter("counter") < 4
DO incrementCounter("counter"); traceln("Throwing exception"); throw new java.lang.RuntimeException("die dammit die")

View File

@ -22,6 +22,7 @@ import net.corda.testing.core.TestIdentity
import net.corda.testing.driver.internal.incrementalPortAllocation
import net.corda.coretesting.internal.rigorousMock
import net.corda.coretesting.internal.stubs.CertificateStoreStubs
import net.corda.nodeapi.internal.protonwrapper.netty.toRevocationConfig
import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID
import org.apache.activemq.artemis.api.core.RoutingType
import org.apache.activemq.artemis.api.core.SimpleString
@ -206,13 +207,22 @@ class AMQPBridgeTest {
val artemisClient = ArtemisMessagingClient(artemisConfig.p2pSslOptions, artemisAddress, MAX_MESSAGE_SIZE)
artemisServer.start()
artemisClient.start()
val bridgeManager = AMQPBridgeManager(artemisConfig.p2pSslOptions, artemisAddress, MAX_MESSAGE_SIZE, artemisConfig.crlCheckSoftFail)
val bridgeManager = AMQPBridgeManager(
artemisConfig.p2pSslOptions.keyStore.get(),
artemisConfig.p2pSslOptions.trustStore.get(),
false,
null,
MAX_MESSAGE_SIZE,
artemisConfig.crlCheckSoftFail.toRevocationConfig(),
false, { ArtemisMessagingClient(artemisConfig.p2pSslOptions, artemisAddress, MAX_MESSAGE_SIZE) }, trace = false,
sslHandshakeTimeout = null,
bridgeConnectionTTLSeconds = 0)
bridgeManager.start()
val artemis = artemisClient.started!!
if (sourceQueueName != null) {
// Local queue for outgoing messages
artemis.session.createQueue(sourceQueueName, RoutingType.ANYCAST, sourceQueueName, true)
bridgeManager.deployBridge(sourceQueueName, listOf(amqpAddress), setOf(BOB.name))
bridgeManager.deployBridge(ALICE_NAME.toString(), sourceQueueName, listOf(amqpAddress), setOf(BOB.name))
}
return Triple(artemisServer, artemisClient, bridgeManager)
}
@ -228,7 +238,6 @@ class AMQPBridgeTest {
doReturn(certificatesDirectory).whenever(it).certificatesDirectory
doReturn(signingCertificateStore).whenever(it).signingCertificateStore
doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions
doReturn(true).whenever(it).crlCheckSoftFail
}
serverConfig.configureWithDevSSLCertificate()
@ -238,7 +247,6 @@ class AMQPBridgeTest {
override val trustStore = serverConfig.p2pSslOptions.trustStore.get()
override val trace: Boolean = true
override val maxMessageSize: Int = maxMessageSize
override val crlCheckSoftFail: Boolean = serverConfig.crlCheckSoftFail
}
return AMQPServer("0.0.0.0",
amqpAddress.port,

View File

@ -29,6 +29,8 @@ import net.corda.coretesting.internal.DEV_INTERMEDIATE_CA
import net.corda.coretesting.internal.DEV_ROOT_CA
import net.corda.coretesting.internal.rigorousMock
import net.corda.coretesting.internal.stubs.CertificateStoreStubs
import net.corda.nodeapi.internal.protonwrapper.netty.RevocationConfig
import net.corda.nodeapi.internal.protonwrapper.netty.toRevocationConfig
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.*
@ -305,7 +307,7 @@ class CertificateRevocationListNodeTests {
}
@Test(timeout=300_000)
fun `Revocation status chceck fails when the CRL distribution point is not set and soft fail is disabled`() {
fun `Revocation status check fails when the CRL distribution point is not set and soft fail is disabled`() {
val crlCheckSoftFail = false
val (amqpServer, _) = createServer(
serverPort,
@ -380,7 +382,6 @@ class CertificateRevocationListNodeTests {
val amqpConfig = object : AMQPConfiguration {
override val keyStore = keyStore
override val trustStore = clientConfig.p2pSslOptions.trustStore.get()
override val crlCheckSoftFail: Boolean = crlCheckSoftFail
override val maxMessageSize: Int = maxMessageSize
}
return Pair(AMQPClient(
@ -404,7 +405,6 @@ class CertificateRevocationListNodeTests {
doReturn(name).whenever(it).myLegalName
doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions
doReturn(signingCertificateStore).whenever(it).signingCertificateStore
doReturn(crlCheckSoftFail).whenever(it).crlCheckSoftFail
}
serverConfig.configureWithDevSSLCertificate()
val nodeCert = (signingCertificateStore to p2pSslConfiguration).recreateNodeCaAndTlsCertificates(nodeCrlDistPoint, tlsCrlDistPoint)
@ -412,7 +412,7 @@ class CertificateRevocationListNodeTests {
val amqpConfig = object : AMQPConfiguration {
override val keyStore = keyStore
override val trustStore = serverConfig.p2pSslOptions.trustStore.get()
override val crlCheckSoftFail: Boolean = crlCheckSoftFail
override val revocationConfig = crlCheckSoftFail.toRevocationConfig()
override val maxMessageSize: Int = maxMessageSize
}
return Pair(AMQPServer(

View File

@ -32,6 +32,7 @@ import net.corda.testing.driver.internal.incrementalPortAllocation
import net.corda.testing.internal.createDevIntermediateCaCertPath
import net.corda.coretesting.internal.rigorousMock
import net.corda.coretesting.internal.stubs.CertificateStoreStubs
import net.corda.nodeapi.internal.protonwrapper.netty.toRevocationConfig
import org.apache.activemq.artemis.api.core.RoutingType
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Assert.assertArrayEquals
@ -39,6 +40,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import javax.net.ssl.*
import kotlin.concurrent.thread
import kotlin.test.assertEquals
@ -341,6 +343,7 @@ class ProtonWrapperTests {
val connection1ID = CordaX500Name.build(connection1.remoteCert!!.subjectX500Principal)
assertEquals("client 0", connection1ID.organisationUnit)
val source1 = connection1.remoteAddress
val client2Connected = amqpClient2.onConnection.toFuture()
amqpClient2.start()
val connection2 = connectionEvents.next()
assertEquals(true, connection2.connected)
@ -353,6 +356,7 @@ class ProtonWrapperTests {
assertEquals(false, connection3.connected)
assertEquals(source1, connection3.remoteAddress)
assertEquals(false, amqpClient1.connected)
client2Connected.get(60, TimeUnit.SECONDS)
assertEquals(true, amqpClient2.connected)
// Now shutdown both
amqpClient2.stop()
@ -362,11 +366,13 @@ class ProtonWrapperTests {
assertEquals(false, amqpClient1.connected)
assertEquals(false, amqpClient2.connected)
// Now restarting one should work
val client1Connected = amqpClient1.onConnection.toFuture()
amqpClient1.start()
val connection5 = connectionEvents.next()
assertEquals(true, connection5.connected)
val connection5ID = CordaX500Name.build(connection5.remoteCert!!.subjectX500Principal)
assertEquals("client 0", connection5ID.organisationUnit)
client1Connected.get(60, TimeUnit.SECONDS)
assertEquals(true, amqpClient1.connected)
assertEquals(false, amqpClient2.connected)
// Cleanup
@ -447,7 +453,6 @@ class ProtonWrapperTests {
override val trustStore = clientTruststore
override val trace: Boolean = true
override val maxMessageSize: Int = maxMessageSize
override val crlCheckSoftFail: Boolean = clientConfig.crlCheckSoftFail
}
return AMQPClient(
listOf(NetworkHostAndPort("localhost", serverPort),
@ -479,7 +484,6 @@ class ProtonWrapperTests {
override val trustStore = clientTruststore
override val trace: Boolean = true
override val maxMessageSize: Int = maxMessageSize
override val crlCheckSoftFail: Boolean = clientConfig.crlCheckSoftFail
}
return AMQPClient(
listOf(NetworkHostAndPort("localhost", serverPort)),
@ -502,7 +506,6 @@ class ProtonWrapperTests {
doReturn(name).whenever(it).myLegalName
doReturn(signingCertificateStore).whenever(it).signingCertificateStore
doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions
doReturn(crlCheckSoftFail).whenever(it).crlCheckSoftFail
}
serverConfig.configureWithDevSSLCertificate()
@ -512,8 +515,8 @@ class ProtonWrapperTests {
override val keyStore = serverKeystore
override val trustStore = serverTruststore
override val trace: Boolean = true
override val revocationConfig = crlCheckSoftFail.toRevocationConfig()
override val maxMessageSize: Int = maxMessageSize
override val crlCheckSoftFail: Boolean = serverConfig.crlCheckSoftFail
}
return AMQPServer(
"0.0.0.0",

View File

@ -50,7 +50,7 @@ class PersistentNetworkMapCacheTest {
@Test(timeout=300_000)
fun addNode() {
val alice = createNodeInfo(listOf(ALICE))
charlieNetMapCache.addNode(alice)
charlieNetMapCache.addOrUpdateNode(alice)
val fromDb = database.transaction {
session.createQuery(
"from ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name}",
@ -62,7 +62,7 @@ class PersistentNetworkMapCacheTest {
@Test(timeout=300_000)
fun `unknown legal name`() {
charlieNetMapCache.addNode(createNodeInfo(listOf(ALICE)))
charlieNetMapCache.addOrUpdateNode(createNodeInfo(listOf(ALICE)))
assertThat(charlieNetMapCache.getNodesByLegalName(DUMMY_NOTARY_NAME)).isEmpty()
assertThat(charlieNetMapCache.getNodeByLegalName(DUMMY_NOTARY_NAME)).isNull()
assertThat(charlieNetMapCache.getPeerByLegalName(DUMMY_NOTARY_NAME)).isNull()
@ -71,13 +71,13 @@ class PersistentNetworkMapCacheTest {
@Test(timeout=300_000)
fun `nodes in distributed service`() {
charlieNetMapCache.addNode(createNodeInfo(listOf(ALICE)))
charlieNetMapCache.addOrUpdateNode(createNodeInfo(listOf(ALICE)))
val distributedIdentity = TestIdentity(DUMMY_NOTARY_NAME)
val distServiceNodeInfos = (1..2).map {
val nodeInfo = createNodeInfo(identities = listOf(TestIdentity.fresh("Org-$it"), distributedIdentity))
charlieNetMapCache.addNode(nodeInfo)
charlieNetMapCache.addOrUpdateNode(nodeInfo)
nodeInfo
}
@ -90,7 +90,7 @@ class PersistentNetworkMapCacheTest {
@Test(timeout=300_000)
fun `get nodes by owning key and by name`() {
val alice = createNodeInfo(listOf(ALICE))
charlieNetMapCache.addNode(alice)
charlieNetMapCache.addOrUpdateNode(alice)
assertThat(charlieNetMapCache.getNodesByLegalIdentityKey(ALICE.publicKey)).containsOnly(alice)
assertThat(charlieNetMapCache.getNodeByLegalName(ALICE.name)).isEqualTo(alice)
}
@ -98,31 +98,31 @@ class PersistentNetworkMapCacheTest {
@Test(timeout=300_000)
fun `get nodes by address`() {
val alice = createNodeInfo(listOf(ALICE))
charlieNetMapCache.addNode(alice)
charlieNetMapCache.addOrUpdateNode(alice)
assertThat(charlieNetMapCache.getNodeByAddress(alice.addresses[0])).isEqualTo(alice)
}
@Test(timeout=300_000)
fun `insert two node infos with the same host and port`() {
val alice = createNodeInfo(listOf(ALICE))
charlieNetMapCache.addNode(alice)
charlieNetMapCache.addOrUpdateNode(alice)
val bob = createNodeInfo(listOf(BOB), address = alice.addresses[0])
charlieNetMapCache.addNode(bob)
charlieNetMapCache.addOrUpdateNode(bob)
val nodeInfos = charlieNetMapCache.allNodes.filter { alice.addresses[0] in it.addresses }
assertThat(nodeInfos).hasSize(2)
}
@Test(timeout=300_000)
fun `negative test - attempt to insert invalid node info`() {
charlieNetMapCache.addNode(createNodeInfo(listOf(LONG_PLC)))
charlieNetMapCache.addOrUpdateNode(createNodeInfo(listOf(LONG_PLC)))
assertThat(charlieNetMapCache.allNodes).hasSize(0)
}
@Test(timeout=300_000)
fun `negative test - attempt to update existing node with invalid node info`() {
charlieNetMapCache.addNode(createNodeInfo(listOf(ALICE)))
charlieNetMapCache.addOrUpdateNode(createNodeInfo(listOf(ALICE)))
val aliceUpdate = TestIdentity(LONG_X500_NAME, ALICE.keyPair)
charlieNetMapCache.addNode(createNodeInfo(listOf(aliceUpdate)))
charlieNetMapCache.addOrUpdateNode(createNodeInfo(listOf(aliceUpdate)))
assertThat(charlieNetMapCache.allNodes).hasSize(1)
assertThat(charlieNetMapCache.getNodeByLegalName(ALICE_NAME)).isNotNull
assertThat(charlieNetMapCache.getNodeByLegalName(LONG_X500_NAME)).isNull()
@ -130,7 +130,7 @@ class PersistentNetworkMapCacheTest {
@Test(timeout=300_000)
fun `negative test - insert two valid node infos and one invalid one`() {
charlieNetMapCache.addNodes(listOf(createNodeInfo(listOf(ALICE)),
charlieNetMapCache.addOrUpdateNodes(listOf(createNodeInfo(listOf(ALICE)),
createNodeInfo(listOf(BOB)),
createNodeInfo(listOf(LONG_PLC))))
assertThat(charlieNetMapCache.allNodes).hasSize(2)
@ -139,7 +139,7 @@ class PersistentNetworkMapCacheTest {
@Test(timeout=300_000)
fun `negative test - insert three valid node infos and two invalid ones`() {
charlieNetMapCache.addNodes(listOf(createNodeInfo(listOf(LONG_PLC)),
charlieNetMapCache.addOrUpdateNodes(listOf(createNodeInfo(listOf(LONG_PLC)),
createNodeInfo(listOf(ALICE)),
createNodeInfo(listOf(BOB)),
createNodeInfo(listOf(CHARLIE)),
@ -150,9 +150,9 @@ class PersistentNetworkMapCacheTest {
@Test(timeout=300_000)
fun `negative test - insert one valid node info then attempt to add one invalid node info and update the existing valid nodeinfo`() {
charlieNetMapCache.addNode(createNodeInfo(listOf(ALICE)))
charlieNetMapCache.addOrUpdateNode(createNodeInfo(listOf(ALICE)))
val aliceUpdate = TestIdentity(LONG_X500_NAME, ALICE.keyPair)
charlieNetMapCache.addNodes(listOf(createNodeInfo(listOf(aliceUpdate)),
charlieNetMapCache.addOrUpdateNodes(listOf(createNodeInfo(listOf(aliceUpdate)),
createNodeInfo(listOf(LONGER_PLC)), createNodeInfo(listOf(BOB))))
assertThat(charlieNetMapCache.allNodes).hasSize(2)
assertThat(charlieNetMapCache.getNodeByLegalName(ALICE_NAME)).isNotNull

View File

@ -49,7 +49,7 @@ open class SharedNodeCmdLineOptions {
var devMode: Boolean? = null
open fun parseConfiguration(configuration: Config): Valid<NodeConfiguration> {
val option = Configuration.Validation.Options(strict = unknownConfigKeysPolicy == UnknownConfigKeysPolicy.FAIL)
val option = Configuration.Options(strict = unknownConfigKeysPolicy == UnknownConfigKeysPolicy.FAIL)
return configuration.parseAsNodeConfiguration(option)
}

View File

@ -151,8 +151,8 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_VALIDITY_WINDOW
import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_COMPOSITE_KEY_ALIAS
import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_KEY_ALIAS
import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_KEY_ALIAS
import net.corda.nodeapi.internal.cryptoservice.CryptoServiceFactory
import net.corda.nodeapi.internal.cryptoservice.SupportedCryptoServices
import net.corda.node.utilities.cryptoservice.CryptoServiceFactory
import net.corda.node.utilities.cryptoservice.SupportedCryptoServices
import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService
import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEvent
import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEventsDistributor
@ -634,7 +634,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
} else {
log.info("Node-info has changed so submitting update. Old node-info was $nodeInfoFromDb")
val newNodeInfo = potentialNodeInfo.copy(serial = platformClock.millis())
networkMapCache.addNode(newNodeInfo)
networkMapCache.addOrUpdateNode(newNodeInfo)
log.info("New node-info: $newNodeInfo")
newNodeInfo
}

View File

@ -79,6 +79,7 @@ import net.corda.nodeapi.internal.bridging.BridgeControlListener
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
import net.corda.nodeapi.internal.protonwrapper.netty.toRevocationConfig
import net.corda.serialization.internal.AMQP_P2P_CONTEXT
import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT
import net.corda.serialization.internal.AMQP_RPC_SERVER_CONTEXT
@ -417,7 +418,15 @@ open class Node(configuration: NodeConfiguration,
failoverCallback = { errorAndTerminate("ArtemisMessagingClient failed. Shutting down.", null) }
)
}
return BridgeControlListener(configuration.p2pSslOptions, networkParameters.maxMessageSize, configuration.crlCheckSoftFail, artemisMessagingClientFactory)
return BridgeControlListener(
configuration.p2pSslOptions.keyStore.get(),
configuration.p2pSslOptions.trustStore.get(),
false,
null,
networkParameters.maxMessageSize,
configuration.crlCheckSoftFail.toRevocationConfig(),
false,
artemisMessagingClientFactory)
}
private fun startLocalRpcBroker(securityManager: RPCSecurityManager): BrokerAddresses? {

View File

@ -26,7 +26,7 @@ internal class ValidateConfigurationCli : CliWrapperBase("validate-configuration
return "for path: \"$pathAsString\": $message"
}
internal fun logRawConfig(config: Config) = logger.info("Actual configuration:\n${V1NodeConfigurationSpec.describe(config, Any?::toConfigValue).render(configRenderingOptions)}")
internal fun logRawConfig(config: Config) = logger.info("Actual configuration:\n${V1NodeConfigurationSpec.describe(config, Any?::toConfigValue, Configuration.Options()).render(configRenderingOptions)}")
}
@Mixin

View File

@ -40,10 +40,11 @@ interface NetworkMapCacheInternal : NetworkMapCache, NetworkMapCacheBase {
* This is used for Artemis bridge lookup process. */
fun getNodesByOwningKeyIndex(identityKeyIndex: String): List<NodeInfo>
/** Adds a node to the local cache (generally only used for adding ourselves). */
fun addNode(node: NodeInfo)
/** Adds (or updates) a node to the local cache (generally only used for adding ourselves). */
fun addOrUpdateNode(node: NodeInfo)
fun addNodes(nodes: List<NodeInfo>)
/** Adds (or updates) nodes to the local cache. */
fun addOrUpdateNodes(nodes: List<NodeInfo>)
/** Removes a node from the local cache. */
fun removeNode(node: NodeInfo)

View File

@ -107,7 +107,7 @@ object ConfigHelper {
newKey.let { key ->
val cfg = ConfigFactory.parseMap(mapOf(key to it.value))
val result = V1NodeConfigurationSpec.validate(cfg, Configuration.Validation.Options(strict = true))
val result = V1NodeConfigurationSpec.validate(cfg, Configuration.Options(strict = true))
val isInvalidProperty = result.errors.any { err -> err is Configuration.Validation.Error.Unknown }
if (isInvalidProperty) {

View File

@ -217,7 +217,7 @@ data class FlowTimeoutConfiguration(
internal typealias Valid<TARGET> = Validated<TARGET, Configuration.Validation.Error>
fun Config.parseAsNodeConfiguration(options: Configuration.Validation.Options = Configuration.Validation.Options(strict = true)): Valid<NodeConfiguration> = V1NodeConfigurationSpec.parse(this, options)
fun Config.parseAsNodeConfiguration(options: Configuration.Options = Configuration.Options(strict = true)): Valid<NodeConfiguration> = V1NodeConfigurationSpec.parse(this, options)
data class NodeH2Settings(
val address: NetworkHostAndPort?

View File

@ -5,11 +5,11 @@ package net.corda.node.services.config.schema.v1
import com.typesafe.config.Config
import com.typesafe.config.ConfigObject
import net.corda.common.configuration.parsing.internal.Configuration
import net.corda.common.configuration.parsing.internal.get
import net.corda.common.configuration.parsing.internal.listOrEmpty
import net.corda.common.configuration.parsing.internal.map
import net.corda.common.configuration.parsing.internal.mapValid
import net.corda.common.configuration.parsing.internal.nested
import net.corda.common.configuration.parsing.internal.withOptions
import net.corda.common.validation.internal.Validated.Companion.invalid
import net.corda.common.validation.internal.Validated.Companion.valid
import net.corda.core.context.AuthServiceId
@ -55,11 +55,12 @@ internal object UserSpec : Configuration.Specification<User>("User") {
private val password by string(sensitive = true)
private val permissions by string().listOrEmpty()
override fun parseValid(configuration: Config): Valid<User> {
val username = configuration[username] ?: configuration[user]
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<User> {
val config = configuration.withOptions(options)
val username = config[username] ?: config[user]
return when (username) {
null -> invalid(Configuration.Validation.Error.MissingValue.forKey("username"))
else -> valid(User(username, configuration[password], configuration[permissions].toSet()))
else -> valid(User(username, config[password], config[permissions].toSet()))
}
}
}
@ -72,11 +73,12 @@ internal object SecurityConfigurationSpec : Configuration.Specification<Security
private val connection by nestedObject(sensitive = true).map(::toProperties).optional()
private val users by nested(UserSpec).list().optional()
override fun parseValid(configuration: Config): Valid<SecurityConfiguration.AuthService.DataSource> {
val type = configuration[type]
val passwordEncryption = configuration[passwordEncryption]
val connection = configuration[connection]
val users = configuration[users]
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<SecurityConfiguration.AuthService.DataSource> {
val config = configuration.withOptions(options)
val type = config[type]
val passwordEncryption = config[passwordEncryption]
val connection = config[connection]
val users = config[users]
return when {
type == AuthDataSourceType.INMEMORY && (users == null || connection != null) -> badValue("\"INMEMORY\" data source type requires \"users\" and cannot specify \"connection\"")
@ -91,15 +93,17 @@ internal object SecurityConfigurationSpec : Configuration.Specification<Security
private val expireAfterSecs by long().mapValid { value -> if (value >= 0) validValue(value) else badValue("cannot be less than 0'") }
private val maxEntries by long().mapValid { value -> if (value >= 0) validValue(value) else badValue("cannot be less than 0'") }
override fun parseValid(configuration: Config): Valid<SecurityConfiguration.AuthService.Options.Cache> {
return valid(SecurityConfiguration.AuthService.Options.Cache(configuration[expireAfterSecs], configuration[maxEntries]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<SecurityConfiguration.AuthService.Options.Cache> {
val config = configuration.withOptions(options)
return valid(SecurityConfiguration.AuthService.Options.Cache(config[expireAfterSecs], config[maxEntries]))
}
}
private val cache by nested(CacheSpec).optional()
override fun parseValid(configuration: Config): Valid<SecurityConfiguration.AuthService.Options> {
return valid(SecurityConfiguration.AuthService.Options(configuration[cache]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<SecurityConfiguration.AuthService.Options> {
val config = configuration.withOptions(options)
return valid(SecurityConfiguration.AuthService.Options(config[cache]))
}
}
@ -107,21 +111,23 @@ internal object SecurityConfigurationSpec : Configuration.Specification<Security
private val id by string().map(::AuthServiceId).optional()
val options by nested(OptionsSpec).optional()
override fun parseValid(configuration: Config): Valid<SecurityConfiguration.AuthService> {
val dataSource = configuration[dataSource]
val id = configuration[id] ?: defaultAuthServiceId(dataSource.type)
val options = configuration[options]
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<SecurityConfiguration.AuthService> {
val config = configuration.withOptions(options)
val dataSource = config[dataSource]
val id = config[id] ?: defaultAuthServiceId(dataSource.type)
val authServiceOptions = config[this.options]
return when {
dataSource.type == AuthDataSourceType.INMEMORY && options?.cache != null -> badValue("no cache supported for \"INMEMORY\" data provider")
else -> valid(SecurityConfiguration.AuthService(dataSource, id, options))
dataSource.type == AuthDataSourceType.INMEMORY && authServiceOptions?.cache != null -> badValue("no cache supported for \"INMEMORY\" data provider")
else -> valid(SecurityConfiguration.AuthService(dataSource, id, authServiceOptions))
}
}
}
private val authService by nested(AuthServiceSpec)
override fun parseValid(configuration: Config): Valid<SecurityConfiguration> {
return valid(SecurityConfiguration(configuration[authService]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<SecurityConfiguration> {
val config = configuration.withOptions(options)
return valid(SecurityConfiguration(config[authService]))
}
}
@ -134,13 +140,15 @@ internal object DevModeOptionsSpec : Configuration.Specification<DevModeOptions>
private val bootstrapSource by string().optional()
private val cordaSource by string().list()
override fun parseValid(configuration: Config): Valid<DJVMOptions> {
return valid(DJVMOptions(configuration[bootstrapSource], configuration[cordaSource]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<DJVMOptions> {
val config = configuration.withOptions(options)
return valid(DJVMOptions(config[bootstrapSource], config[cordaSource]))
}
}
override fun parseValid(configuration: Config): Valid<DevModeOptions> {
return valid(DevModeOptions(configuration[disableCheckpointChecker], configuration[allowCompatibilityZone], configuration[djvm]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<DevModeOptions> {
val config = configuration.withOptions(options)
return valid(DevModeOptions(config[disableCheckpointChecker], config[allowCompatibilityZone], config[djvm]))
}
}
@ -151,8 +159,9 @@ internal object NetworkServicesConfigSpec : Configuration.Specification<NetworkS
private val inferred by boolean().optional().withDefaultValue(false)
private val csrToken by string(sensitive = true).optional()
override fun parseValid(configuration: Config): Valid<NetworkServicesConfig> {
return valid(NetworkServicesConfig(configuration[doormanURL], configuration[networkMapURL], configuration[pnm], configuration[inferred], configuration[csrToken]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<NetworkServicesConfig> {
val config = configuration.withOptions(options)
return valid(NetworkServicesConfig(config[doormanURL], config[networkMapURL], config[pnm], config[inferred], config[csrToken]))
}
}
@ -160,22 +169,24 @@ internal object NetworkParameterAcceptanceSettingsSpec :
Configuration.Specification<NetworkParameterAcceptanceSettings>("NetworkParameterAcceptanceSettings") {
private val autoAcceptEnabled by boolean().optional().withDefaultValue(true)
private val excludedAutoAcceptableParameters by string().listOrEmpty()
override fun parseValid(configuration: Config): Valid<NetworkParameterAcceptanceSettings> {
return valid(NetworkParameterAcceptanceSettings(configuration[autoAcceptEnabled],
configuration[excludedAutoAcceptableParameters].toSet())
override fun parseValid(configuration: Config, options: Configuration.Options):
Valid<NetworkParameterAcceptanceSettings> {
val config = configuration.withOptions(options)
return valid(NetworkParameterAcceptanceSettings(config[autoAcceptEnabled],
config[excludedAutoAcceptableParameters].toSet())
)
}
}
@Suppress("DEPRECATION")
internal object CertChainPolicyConfigSpec : Configuration.Specification<CertChainPolicyConfig>("CertChainPolicyConfig") {
private val role by string()
private val policy by enum(CertChainPolicyType::class)
private val trustedAliases by string().listOrEmpty()
override fun parseValid(configuration: Config): Valid<CertChainPolicyConfig> {
return valid(CertChainPolicyConfig(configuration[role], configuration[policy], configuration[trustedAliases].toSet()))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<CertChainPolicyConfig> {
val config = configuration.withOptions(options)
return valid(CertChainPolicyConfig(config[role], config[policy], config[trustedAliases].toSet()))
}
}
@ -184,8 +195,9 @@ internal object FlowTimeoutConfigurationSpec : Configuration.Specification<FlowT
private val maxRestartCount by int()
private val backoffBase by double()
override fun parseValid(configuration: Config): Valid<FlowTimeoutConfiguration> {
return valid(FlowTimeoutConfiguration(configuration[timeout], configuration[maxRestartCount], configuration[backoffBase]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<FlowTimeoutConfiguration> {
val config = configuration.withOptions(options)
return valid(FlowTimeoutConfiguration(config[timeout], config[maxRestartCount], config[backoffBase]))
}
}
@ -198,8 +210,9 @@ internal object NotaryConfigSpec : Configuration.Specification<NotaryConfig>("No
private val raft by nested(RaftConfigSpec).optional()
private val bftSMaRt by nested(BFTSmartConfigSpec).optional()
override fun parseValid(configuration: Config): Valid<NotaryConfig> {
return valid(NotaryConfig(configuration[validating], configuration[serviceLegalName], configuration[className], configuration[etaMessageThresholdSeconds], configuration[extraConfig], configuration[raft], configuration[bftSMaRt]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<NotaryConfig> {
val config = configuration.withOptions(options)
return valid(NotaryConfig(config[validating], config[serviceLegalName], config[className], config[etaMessageThresholdSeconds], config[extraConfig], config[raft], config[bftSMaRt]))
}
}
@ -207,8 +220,9 @@ internal object RaftConfigSpec : Configuration.Specification<RaftConfig>("RaftCo
private val nodeAddress by string().mapValid(::toNetworkHostAndPort)
private val clusterAddresses by string().mapValid(::toNetworkHostAndPort).listOrEmpty()
override fun parseValid(configuration: Config): Valid<RaftConfig> {
return valid(RaftConfig(configuration[nodeAddress], configuration[clusterAddresses]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<RaftConfig> {
val config = configuration.withOptions(options)
return valid(RaftConfig(config[nodeAddress], config[clusterAddresses]))
}
}
@ -218,19 +232,20 @@ internal object BFTSmartConfigSpec : Configuration.Specification<BFTSmartConfig>
private val debug by boolean().optional().withDefaultValue(false)
private val exposeRaces by boolean().optional().withDefaultValue(false)
override fun parseValid(configuration: Config): Valid<BFTSmartConfig> {
return valid(BFTSmartConfig(configuration[replicaId], configuration[clusterAddresses], configuration[debug], configuration[exposeRaces]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<BFTSmartConfig> {
val config = configuration.withOptions(options)
return valid(BFTSmartConfig(config[replicaId], config[clusterAddresses], config[debug], config[exposeRaces]))
}
}
internal object NodeRpcSettingsSpec : Configuration.Specification<NodeRpcSettings>("NodeRpcSettings") {
internal object BrokerRpcSslOptionsSpec : Configuration.Specification<BrokerRpcSslOptions>("BrokerRpcSslOptions") {
private val keyStorePath by string().mapValid(::toPath)
private val keyStorePassword by string(sensitive = true)
override fun parseValid(configuration: Config): Valid<BrokerRpcSslOptions> {
return valid(BrokerRpcSslOptions(configuration[keyStorePath], configuration[keyStorePassword]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<BrokerRpcSslOptions> {
val config = configuration.withOptions(options)
return valid(BrokerRpcSslOptions(config[keyStorePath], config[keyStorePassword]))
}
}
@ -240,18 +255,18 @@ internal object NodeRpcSettingsSpec : Configuration.Specification<NodeRpcSetting
private val useSsl by boolean().optional().withDefaultValue(NodeRpcSettings.Defaults.useSsl)
private val ssl by nested(BrokerRpcSslOptionsSpec).optional()
override fun parseValid(configuration: Config): Valid<NodeRpcSettings> {
return valid(NodeRpcSettings(configuration[address], configuration[adminAddress], configuration[standAloneBroker], configuration[useSsl], configuration[ssl]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<NodeRpcSettings> {
val config = configuration.withOptions(options)
return valid(NodeRpcSettings(config[address], config[adminAddress], config[standAloneBroker], config[useSsl], config[ssl]))
}
}
internal object SSHDConfigurationSpec : Configuration.Specification<SSHDConfiguration>("SSHDConfiguration") {
private val port by int()
override fun parseValid(configuration: Config): Valid<SSHDConfiguration> = attempt<SSHDConfiguration, IllegalArgumentException> { SSHDConfiguration(configuration[port]) }
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<SSHDConfiguration> = attempt<SSHDConfiguration, IllegalArgumentException> { SSHDConfiguration(configuration.withOptions(options)[port]) }
}
internal object DatabaseConfigSpec : Configuration.Specification<DatabaseConfig>("DatabaseConfig") {
private val initialiseSchema by boolean().optional().withDefaultValue(DatabaseConfig.Defaults.initialiseSchema)
private val initialiseAppSchema by enum(SchemaInitializationType::class).optional().withDefaultValue(DatabaseConfig.Defaults.initialiseAppSchema)
@ -259,16 +274,18 @@ internal object DatabaseConfigSpec : Configuration.Specification<DatabaseConfig>
private val exportHibernateJMXStatistics by boolean().optional().withDefaultValue(DatabaseConfig.Defaults.exportHibernateJMXStatistics)
private val mappedSchemaCacheSize by long().optional().withDefaultValue(DatabaseConfig.Defaults.mappedSchemaCacheSize)
override fun parseValid(configuration: Config): Valid<DatabaseConfig> {
return valid(DatabaseConfig(configuration[initialiseSchema], configuration[initialiseAppSchema], configuration[transactionIsolationLevel], configuration[exportHibernateJMXStatistics], configuration[mappedSchemaCacheSize]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<DatabaseConfig> {
val config = configuration.withOptions(options)
return valid(DatabaseConfig(config[initialiseSchema], config[initialiseAppSchema], config[transactionIsolationLevel], config[exportHibernateJMXStatistics], config[mappedSchemaCacheSize]))
}
}
internal object NodeH2SettingsSpec : Configuration.Specification<NodeH2Settings>("NodeH2Settings") {
private val address by string().mapValid(::toNetworkHostAndPort).optional()
override fun parseValid(configuration: Config): Valid<NodeH2Settings> {
return valid(NodeH2Settings(configuration[address]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<NodeH2Settings> {
val config = configuration.withOptions(options)
return valid(NodeH2Settings(config[address]))
}
}
@ -277,14 +294,16 @@ internal object FlowOverridesConfigSpec : Configuration.Specification<FlowOverri
private val initiator by string()
private val responder by string()
override fun parseValid(configuration: Config): Valid<FlowOverride> {
return valid(FlowOverride(configuration[initiator], configuration[responder]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<FlowOverride> {
val config = configuration.withOptions(options)
return valid(FlowOverride(config[initiator], config[responder]))
}
}
private val overrides by nested(FlowOverridesConfigSpec.SingleSpec).listOrEmpty()
override fun parseValid(configuration: Config): Valid<FlowOverrideConfig> {
return valid(FlowOverrideConfig(configuration[overrides]))
override fun parseValid(configuration: Config, options: Configuration.Options): Valid<FlowOverrideConfig> {
val config = configuration.withOptions(options)
return valid(FlowOverrideConfig(config[overrides]))
}
}

View File

@ -70,67 +70,68 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
@Suppress("unused")
private val systemProperties by nestedObject().optional()
override fun parseValid(configuration: Config): Validated<NodeConfiguration, Configuration.Validation.Error> {
override fun parseValid(configuration: Config, options: Configuration.Options): Validated<NodeConfiguration, Configuration.Validation.Error> {
val config = configuration.withOptions(options)
val messagingServerExternal = configuration[messagingServerExternal] ?: Defaults.messagingServerExternal(configuration[messagingServerAddress])
val database = configuration[database] ?: Defaults.database(configuration[devMode])
val baseDirectoryPath = configuration[baseDirectory]
val cordappDirectories = configuration[cordappDirectories] ?: Defaults.cordappsDirectories(baseDirectoryPath)
val messagingServerExternal = config[messagingServerExternal] ?: Defaults.messagingServerExternal(config[messagingServerAddress])
val database = config[database] ?: Defaults.database(config[devMode])
val baseDirectoryPath = config[baseDirectory]
val cordappDirectories = config[cordappDirectories] ?: Defaults.cordappsDirectories(baseDirectoryPath)
val result = try {
valid<NodeConfigurationImpl, Configuration.Validation.Error>(NodeConfigurationImpl(
baseDirectory = baseDirectoryPath,
myLegalName = configuration[myLegalName],
emailAddress = configuration[emailAddress],
p2pAddress = configuration[p2pAddress],
keyStorePassword = configuration[keyStorePassword],
trustStorePassword = configuration[trustStorePassword],
crlCheckSoftFail = configuration[crlCheckSoftFail],
dataSourceProperties = configuration[dataSourceProperties],
rpcUsers = configuration[rpcUsers],
verifierType = configuration[verifierType],
flowTimeout = configuration[flowTimeout],
rpcSettings = configuration[rpcSettings],
messagingServerAddress = configuration[messagingServerAddress],
notary = configuration[notary],
flowOverrides = configuration[flowOverrides],
additionalP2PAddresses = configuration[additionalP2PAddresses],
additionalNodeInfoPollingFrequencyMsec = configuration[additionalNodeInfoPollingFrequencyMsec],
jmxMonitoringHttpPort = configuration[jmxMonitoringHttpPort],
security = configuration[security],
devMode = configuration[devMode],
devModeOptions = configuration[devModeOptions],
compatibilityZoneURL = configuration[compatibilityZoneURL],
networkServices = configuration[networkServices],
certificateChainCheckPolicies = configuration[certificateChainCheckPolicies],
myLegalName = config[myLegalName],
emailAddress = config[emailAddress],
p2pAddress = config[p2pAddress],
keyStorePassword = config[keyStorePassword],
trustStorePassword = config[trustStorePassword],
crlCheckSoftFail = config[crlCheckSoftFail],
dataSourceProperties = config[dataSourceProperties],
rpcUsers = config[rpcUsers],
verifierType = config[verifierType],
flowTimeout = config[flowTimeout],
rpcSettings = config[rpcSettings],
messagingServerAddress = config[messagingServerAddress],
notary = config[notary],
flowOverrides = config[flowOverrides],
additionalP2PAddresses = config[additionalP2PAddresses],
additionalNodeInfoPollingFrequencyMsec = config[additionalNodeInfoPollingFrequencyMsec],
jmxMonitoringHttpPort = config[jmxMonitoringHttpPort],
security = config[security],
devMode = config[devMode],
devModeOptions = config[devModeOptions],
compatibilityZoneURL = config[compatibilityZoneURL],
networkServices = config[networkServices],
certificateChainCheckPolicies = config[certificateChainCheckPolicies],
messagingServerExternal = messagingServerExternal,
useTestClock = configuration[useTestClock],
lazyBridgeStart = configuration[lazyBridgeStart],
detectPublicIp = configuration[detectPublicIp],
sshd = configuration[sshd],
localShellAllowExitInSafeMode = configuration[localShellAllowExitInSafeMode],
localShellUnsafe = configuration[localShellUnsafe],
useTestClock = config[useTestClock],
lazyBridgeStart = config[lazyBridgeStart],
detectPublicIp = config[detectPublicIp],
sshd = config[sshd],
localShellAllowExitInSafeMode = config[localShellAllowExitInSafeMode],
localShellUnsafe = config[localShellUnsafe],
database = database,
noLocalShell = configuration[noLocalShell],
attachmentCacheBound = configuration[attachmentCacheBound],
extraNetworkMapKeys = configuration[extraNetworkMapKeys],
tlsCertCrlDistPoint = configuration[tlsCertCrlDistPoint],
tlsCertCrlIssuer = configuration[tlsCertCrlIssuer],
h2Settings = configuration[h2Settings],
flowMonitorPeriodMillis = configuration[flowMonitorPeriodMillis],
flowMonitorSuspensionLoggingThresholdMillis = configuration[flowMonitorSuspensionLoggingThresholdMillis],
jmxReporterType = configuration[jmxReporterType],
rpcAddress = configuration[rpcAddress],
transactionCacheSizeMegaBytes = configuration[transactionCacheSizeMegaBytes],
attachmentContentCacheSizeMegaBytes = configuration[attachmentContentCacheSizeMegaBytes],
h2port = configuration[h2port],
jarDirs = configuration[jarDirs],
noLocalShell = config[noLocalShell],
attachmentCacheBound = config[attachmentCacheBound],
extraNetworkMapKeys = config[extraNetworkMapKeys],
tlsCertCrlDistPoint = config[tlsCertCrlDistPoint],
tlsCertCrlIssuer = config[tlsCertCrlIssuer],
h2Settings = config[h2Settings],
flowMonitorPeriodMillis = config[flowMonitorPeriodMillis],
flowMonitorSuspensionLoggingThresholdMillis = config[flowMonitorSuspensionLoggingThresholdMillis],
jmxReporterType = config[jmxReporterType],
rpcAddress = config[rpcAddress],
transactionCacheSizeMegaBytes = config[transactionCacheSizeMegaBytes],
attachmentContentCacheSizeMegaBytes = config[attachmentContentCacheSizeMegaBytes],
h2port = config[h2port],
jarDirs = config[jarDirs],
cordappDirectories = cordappDirectories.map { baseDirectoryPath.resolve(it) },
cordappSignerKeyFingerprintBlacklist = configuration[cordappSignerKeyFingerprintBlacklist],
blacklistedAttachmentSigningKeys = configuration[blacklistedAttachmentSigningKeys],
networkParameterAcceptanceSettings = configuration[networkParameterAcceptanceSettings],
configurationWithOptions = ConfigurationWithOptions(configuration, Configuration.Validation.Options.defaults),
flowExternalOperationThreadPoolSize = configuration[flowExternalOperationThreadPoolSize],
quasarExcludePackages = configuration[quasarExcludePackages]
cordappSignerKeyFingerprintBlacklist = config[cordappSignerKeyFingerprintBlacklist],
blacklistedAttachmentSigningKeys = config[blacklistedAttachmentSigningKeys],
networkParameterAcceptanceSettings = config[networkParameterAcceptanceSettings],
configurationWithOptions = ConfigurationWithOptions(configuration, Configuration.Options.defaults),
flowExternalOperationThreadPoolSize = config[flowExternalOperationThreadPoolSize],
quasarExcludePackages = config[quasarExcludePackages]
))
} catch (e: Exception) {
return when (e) {

Some files were not shown because too many files have changed in this diff Show More