Merge remote-tracking branch 'open/master' into kat-merge-20180517

This commit is contained in:
Katelyn Baker 2018-05-17 15:04:51 +01:00
commit 74c5cffd38
80 changed files with 6509 additions and 3351 deletions

File diff suppressed because it is too large Load Diff

View File

@ -120,9 +120,7 @@ buildscript {
classpath "net.corda.plugins:quasar-utils:$gradle_plugins_version" classpath "net.corda.plugins:quasar-utils:$gradle_plugins_version"
classpath "net.corda.plugins:cordformation:$gradle_plugins_version" classpath "net.corda.plugins:cordformation:$gradle_plugins_version"
classpath "net.corda.plugins:cordapp:$gradle_plugins_version" classpath "net.corda.plugins:cordapp:$gradle_plugins_version"
//TODO Anthony- this should be changed back to $gradle_plugins_version when the api-scaner is fixed classpath "net.corda.plugins:api-scanner:$gradle_plugins_version"
// classpath "net.corda.plugins:api-scanner:$gradle_plugins_version"
classpath "net.corda.plugins:api-scanner:4.0.15"
classpath 'com.github.ben-manes:gradle-versions-plugin:0.15.0' classpath 'com.github.ben-manes:gradle-versions-plugin:0.15.0'
classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"

View File

@ -51,9 +51,9 @@ class BlacklistKotlinClosureTest : IntegrationTest() {
driver(DriverParameters(startNodesInProcess = true)) { driver(DriverParameters(startNodesInProcess = true)) {
val rpc = startNode(providedName = ALICE_NAME).getOrThrow().rpc val rpc = startNode(providedName = ALICE_NAME).getOrThrow().rpc
val packet = Packet { EVIL } val packet = Packet { EVIL }
assertThatExceptionOfType(KryoException::class.java) assertThatExceptionOfType(RPCException::class.java)
.isThrownBy { rpc.startFlow(::FlowC, packet) } .isThrownBy { rpc.startFlow(::FlowC, packet) }
.withMessageContaining("is not annotated or on the whitelist, so cannot be used in serialization") .withMessageContaining("is not on the whitelist or annotated with @CordaSerializable")
} }
} }
} }

View File

@ -10,9 +10,10 @@
package net.corda.client.rpc package net.corda.client.rpc
import net.corda.client.rpc.internal.CordaRPCClientConfigurationImpl
import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
import net.corda.client.rpc.internal.RPCClient import net.corda.client.rpc.internal.RPCClient
import net.corda.client.rpc.internal.serialization.kryo.KryoClientSerializationScheme import net.corda.client.rpc.internal.CordaRPCClientConfigurationImpl
import net.corda.core.context.Actor import net.corda.core.context.Actor
import net.corda.core.context.Trace import net.corda.core.context.Trace
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
@ -21,7 +22,7 @@ import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT import net.corda.nodeapi.internal.serialization.AMQP_RPC_CLIENT_CONTEXT
import java.time.Duration import java.time.Duration
/** /**
@ -121,7 +122,9 @@ class CordaRPCClient private constructor(
private val haAddressPool: List<NetworkHostAndPort> = emptyList() private val haAddressPool: List<NetworkHostAndPort> = emptyList()
) { ) {
@JvmOverloads @JvmOverloads
constructor(hostAndPort: NetworkHostAndPort, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default()) : this(hostAndPort, configuration, null) constructor(hostAndPort: NetworkHostAndPort,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default())
: this(hostAndPort, configuration, null)
/** /**
* @param haAddressPool A list of [NetworkHostAndPort] representing the addresses of servers in HA mode. * @param haAddressPool A list of [NetworkHostAndPort] representing the addresses of servers in HA mode.
@ -173,7 +176,7 @@ class CordaRPCClient private constructor(
effectiveSerializationEnv effectiveSerializationEnv
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
try { try {
KryoClientSerializationScheme.initialiseSerialization(classLoader) AMQPClientSerializationScheme.initialiseSerialization()
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
// Race e.g. two of these constructed in parallel, ignore. // Race e.g. two of these constructed in parallel, ignore.
} }
@ -185,12 +188,12 @@ class CordaRPCClient private constructor(
RPCClient( RPCClient(
tcpTransport(ConnectionDirection.Outbound(), hostAndPort, config = sslConfiguration), tcpTransport(ConnectionDirection.Outbound(), hostAndPort, config = sslConfiguration),
configuration, configuration,
if (classLoader != null) KRYO_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else KRYO_RPC_CLIENT_CONTEXT) if (classLoader != null) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else AMQP_RPC_CLIENT_CONTEXT)
} else { } else {
RPCClient(haAddressPool, RPCClient(haAddressPool,
sslConfiguration, sslConfiguration,
configuration, configuration,
if (classLoader != null) KRYO_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else KRYO_RPC_CLIENT_CONTEXT) if (classLoader != null) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else AMQP_RPC_CLIENT_CONTEXT)
} }
} }

View File

@ -20,7 +20,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder
import net.corda.client.rpc.CordaRPCClientConfiguration import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.client.rpc.RPCException import net.corda.client.rpc.RPCException
import net.corda.client.rpc.RPCSinceVersion import net.corda.client.rpc.RPCSinceVersion
import net.corda.client.rpc.internal.serialization.kryo.RpcClientObservableSerializer import net.corda.client.rpc.internal.serialization.amqp.RpcClientObservableSerializer
import net.corda.core.context.Actor import net.corda.core.context.Actor
import net.corda.core.context.Trace import net.corda.core.context.Trace
import net.corda.core.context.Trace.InvocationId import net.corda.core.context.Trace.InvocationId

View File

@ -0,0 +1,63 @@
package net.corda.client.rpc.internal.serialization.amqp
import net.corda.core.cordapp.Cordapp
import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationCustomSerializer
import net.corda.core.serialization.internal.SerializationEnvironment
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.nodeapi.internal.serialization.*
import net.corda.nodeapi.internal.serialization.amqp.AbstractAMQPSerializationScheme
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
import net.corda.nodeapi.internal.serialization.amqp.amqpMagic
import java.util.concurrent.ConcurrentHashMap
import net.corda.nodeapi.internal.serialization.amqp.custom.RxNotificationSerializer
/**
* When set as the serialization scheme for a process, sets it to be the Corda AMQP implementation.
* This scheme is for use by the RPC Client calls.
*/
class AMQPClientSerializationScheme(
cordappCustomSerializers: Set<SerializationCustomSerializer<*,*>>,
serializerFactoriesForContexts: MutableMap<Pair<ClassWhitelist, ClassLoader>, SerializerFactory>
) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) {
constructor(cordapps: List<Cordapp>) : this(cordapps.customSerializers, ConcurrentHashMap())
@Suppress("UNUSED")
constructor() : this(emptySet(), ConcurrentHashMap())
companion object {
/** Call from main only. */
fun initialiseSerialization() {
nodeSerializationEnv = createSerializationEnv()
}
fun createSerializationEnv(): SerializationEnvironment {
return SerializationEnvironmentImpl(
SerializationFactoryImpl().apply {
registerScheme(AMQPClientSerializationScheme(emptyList()))
},
storageContext = AMQP_STORAGE_CONTEXT,
p2pContext = AMQP_P2P_CONTEXT,
rpcClientContext = AMQP_RPC_CLIENT_CONTEXT,
rpcServerContext = AMQP_RPC_SERVER_CONTEXT)
}
}
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase) =
magic == amqpMagic && (
target == SerializationContext.UseCase.RPCClient || target == SerializationContext.UseCase.P2P)
override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory {
return SerializerFactory(context.whitelist, ClassLoader.getSystemClassLoader()).apply {
register(RpcClientObservableSerializer)
register(RpcClientCordaFutureSerializer(this))
register(RxNotificationSerializer(this))
}
}
override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory {
throw UnsupportedOperationException()
}
}

View File

@ -0,0 +1,35 @@
package net.corda.client.rpc.internal.serialization.amqp
import net.corda.core.concurrent.CordaFuture
import net.corda.core.toFuture
import net.corda.core.toObservable
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
import rx.Observable
import java.io.NotSerializableException
/**
* Serializer for [CordaFuture] instances that can only deserialize such objects (just as the server
* side can only serialize them). Futures will have been converted to an Rx [Observable] for serialization.
*/
class RpcClientCordaFutureSerializer (factory: SerializerFactory)
: CustomSerializer.Proxy<CordaFuture<*>, RpcClientCordaFutureSerializer.FutureProxy>(
CordaFuture::class.java,
RpcClientCordaFutureSerializer.FutureProxy::class.java, factory
) {
override fun fromProxy(proxy: FutureProxy): CordaFuture<*> {
try {
return proxy.observable.toFuture()
} catch (e: NotSerializableException) {
throw NotSerializableException("Failed to deserialize Future from proxy Observable - ${e.message}\n").apply {
initCause(e.cause)
}
}
}
override fun toProxy(obj: CordaFuture<*>): FutureProxy {
throw UnsupportedOperationException()
}
data class FutureProxy(val observable: Observable<*>)
}

View File

@ -0,0 +1,127 @@
package net.corda.client.rpc.internal.serialization.amqp
import net.corda.client.rpc.internal.ObservableContext
import net.corda.core.context.Trace
import net.corda.core.serialization.SerializationContext
import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.internal.serialization.amqp.*
import org.apache.qpid.proton.codec.Data
import rx.Notification
import rx.Observable
import rx.subjects.UnicastSubject
import java.io.NotSerializableException
import java.lang.reflect.Type
import java.time.Instant
import java.util.concurrent.atomic.AtomicInteger
import javax.transaction.NotSupportedException
/**
* Serializer for Rx[Observable] instances for the RPC Client library. Can only be used to deserialize such objects,
* just as the corresponding RPC server side code can only serialize them. Observables are only notionally serialized,
* what is actually sent is a reference to the observable that can then be subscribed to.
*/
object RpcClientObservableSerializer : CustomSerializer.Implements<Observable<*>>(Observable::class.java) {
private object RpcObservableContextKey
fun createContext(
serializationContext: SerializationContext,
observableContext: ObservableContext
) = serializationContext.withProperty(RpcObservableContextKey, observableContext)
private fun <T> pinInSubscriptions(observable: Observable<T>, hardReferenceStore: MutableSet<Observable<*>>): Observable<T> {
val refCount = AtomicInteger(0)
return observable.doOnSubscribe {
if (refCount.getAndIncrement() == 0) {
require(hardReferenceStore.add(observable)) {
"Reference store already contained reference $this on add"
}
}
}.doOnUnsubscribe {
if (refCount.decrementAndGet() == 0) {
require(hardReferenceStore.remove(observable)) {
"Reference store did not contain reference $this on remove"
}
}
}
}
override val schemaForDocumentation = Schema(
listOf(
CompositeType(
name = type.toString(),
label = "",
provides = emptyList(),
descriptor = descriptor,
fields = listOf(
Field(
name = "observableId",
type = "string",
requires = emptyList(),
default = null,
label = null,
mandatory = true,
multiple = false),
Field(
name = "observableInstant",
type = "long",
requires = emptyList(),
default = null,
label = null,
mandatory = true,
multiple = false)
))))
/**
* Converts the serialized form, a blob, back into an Observable
*/
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
context: SerializationContext
): Observable<*> {
if (RpcObservableContextKey !in context.properties) {
throw NotSerializableException("Missing Observable Context Key on Client Context")
}
val observableContext =
context.properties[RpcClientObservableSerializer.RpcObservableContextKey] as ObservableContext
if (obj !is List<*>) throw NotSerializableException("Input must be a serialised list")
if (obj.size != 2) throw NotSerializableException("Expecting two elements, have ${obj.size}")
val observableId: Trace.InvocationId = Trace.InvocationId((obj[0] as String), Instant.ofEpochMilli((obj[1] as Long)))
val observable = UnicastSubject.create<Notification<*>>()
require(observableContext.observableMap.getIfPresent(observableId) == null) {
"Multiple Observables arrived with the same ID $observableId"
}
val rpcCallSite = getRpcCallSite(context, observableContext)
observableContext.observableMap.put(observableId, observable)
observableContext.callSiteMap?.put(observableId, rpcCallSite)
// We pin all Observables into a hard reference store (rooted in the RPC proxy) on subscription so that users
// don't need to store a reference to the Observables themselves.
return pinInSubscriptions(observable, observableContext.hardReferenceStore).doOnUnsubscribe {
// This causes Future completions to give warnings because the corresponding OnComplete sent from the server
// will arrive after the client unsubscribes from the observable and consequently invalidates the mapping.
// The unsubscribe is due to [ObservableToFuture]'s use of first().
observableContext.observableMap.invalidate(observableId)
}.dematerialize<Any>()
}
private fun getRpcCallSite(context: SerializationContext, observableContext: ObservableContext): Throwable? {
val rpcRequestOrObservableId = context.properties[RPCApi.RpcRequestOrObservableIdKey] as Trace.InvocationId
return observableContext.callSiteMap?.get(rpcRequestOrObservableId)
}
override fun writeDescribedObject(
obj: Observable<*>,
data: Data,
type: Type,
output: SerializationOutput,
context: SerializationContext
) {
throw NotSupportedException()
}
}

View File

@ -1,60 +0,0 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package net.corda.client.rpc.internal.serialization.kryo
import com.esotericsoftware.kryo.pool.KryoPool
import net.corda.core.serialization.SerializationContext
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
import net.corda.core.serialization.internal.SerializationEnvironment
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme
import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme
import net.corda.nodeapi.internal.serialization.kryo.DefaultKryoCustomizer
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
import net.corda.nodeapi.internal.serialization.kryo.RPCKryo
class KryoClientSerializationScheme : AbstractKryoSerializationScheme() {
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean {
return magic == kryoMagic && (target == SerializationContext.UseCase.RPCClient || target == SerializationContext.UseCase.P2P)
}
override fun rpcClientKryoPool(context: SerializationContext): KryoPool {
return KryoPool.Builder {
DefaultKryoCustomizer.customize(RPCKryo(RpcClientObservableSerializer, context), publicKeySerializer).apply {
classLoader = context.deserializationClassLoader
}
}.build()
}
// We're on the client and don't have access to server classes.
override fun rpcServerKryoPool(context: SerializationContext): KryoPool = throw UnsupportedOperationException()
companion object {
/** Call from main only. */
fun initialiseSerialization(classLoader: ClassLoader? = null) {
nodeSerializationEnv = createSerializationEnv(classLoader)
}
fun createSerializationEnv(classLoader: ClassLoader? = null): SerializationEnvironment {
return SerializationEnvironmentImpl(
SerializationFactoryImpl().apply {
registerScheme(KryoClientSerializationScheme())
registerScheme(AMQPClientSerializationScheme(emptyList()))
},
if (classLoader != null) AMQP_P2P_CONTEXT.withClassLoader(classLoader) else AMQP_P2P_CONTEXT,
rpcClientContext = if (classLoader != null) KRYO_RPC_CLIENT_CONTEXT.withClassLoader(classLoader) else KRYO_RPC_CLIENT_CONTEXT)
}
}
}

View File

@ -1,75 +0,0 @@
package net.corda.client.rpc.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 net.corda.client.rpc.internal.ObservableContext
import net.corda.core.context.Trace
import net.corda.core.serialization.SerializationContext
import net.corda.nodeapi.RPCApi
import rx.Notification
import rx.Observable
import rx.subjects.UnicastSubject
import java.time.Instant
import java.util.concurrent.atomic.AtomicInteger
/**
* A [Serializer] to deserialise Observables once the corresponding Kryo instance has been provided with an [ObservableContext].
*/
object RpcClientObservableSerializer : Serializer<Observable<*>>() {
private object RpcObservableContextKey
fun createContext(serializationContext: SerializationContext, observableContext: ObservableContext): SerializationContext {
return serializationContext.withProperty(RpcObservableContextKey, observableContext)
}
private fun <T> pinInSubscriptions(observable: Observable<T>, hardReferenceStore: MutableSet<Observable<*>>): Observable<T> {
val refCount = AtomicInteger(0)
return observable.doOnSubscribe {
if (refCount.getAndIncrement() == 0) {
require(hardReferenceStore.add(observable)) { "Reference store already contained reference $this on add" }
}
}.doOnUnsubscribe {
if (refCount.decrementAndGet() == 0) {
require(hardReferenceStore.remove(observable)) { "Reference store did not contain reference $this on remove" }
}
}
}
override fun read(kryo: Kryo, input: Input, type: Class<Observable<*>>): Observable<Any> {
val observableContext = kryo.context[RpcObservableContextKey] as ObservableContext
val observableId = input.readInvocationId() ?: throw IllegalStateException("Unable to read invocationId from Input.")
val observable = UnicastSubject.create<Notification<*>>()
require(observableContext.observableMap.getIfPresent(observableId) == null) {
"Multiple Observables arrived with the same ID $observableId"
}
val rpcCallSite = getRpcCallSite(kryo, observableContext)
observableContext.observableMap.put(observableId, observable)
observableContext.callSiteMap?.put(observableId, rpcCallSite)
// We pin all Observables into a hard reference store (rooted in the RPC proxy) on subscription so that users
// don't need to store a reference to the Observables themselves.
return pinInSubscriptions(observable, observableContext.hardReferenceStore).doOnUnsubscribe {
// This causes Future completions to give warnings because the corresponding OnComplete sent from the server
// will arrive after the client unsubscribes from the observable and consequently invalidates the mapping.
// The unsubscribe is due to [ObservableToFuture]'s use of first().
observableContext.observableMap.invalidate(observableId)
}.dematerialize()
}
private fun Input.readInvocationId(): Trace.InvocationId? {
val value = readString() ?: return null
val timestamp = readLong()
return Trace.InvocationId(value, Instant.ofEpochMilli(timestamp))
}
override fun write(kryo: Kryo, output: Output, observable: Observable<*>) {
throw UnsupportedOperationException("Cannot serialise Observables on the client side")
}
private fun getRpcCallSite(kryo: Kryo, observableContext: ObservableContext): Throwable? {
val rpcRequestOrObservableId = kryo.context[RPCApi.RpcRequestOrObservableIdKey] as Trace.InvocationId
return observableContext.callSiteMap?.get(rpcRequestOrObservableId)
}
}

View File

@ -10,6 +10,7 @@
package net.corda.client.rpc package net.corda.client.rpc
import net.corda.core.CordaRuntimeException
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.doneFuture
import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.openFuture
@ -21,6 +22,7 @@ import net.corda.testing.node.internal.RPCDriverDSL
import net.corda.testing.node.internal.rpcDriver import net.corda.testing.node.internal.rpcDriver
import net.corda.testing.node.internal.rpcTestUser import net.corda.testing.node.internal.rpcTestUser
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.Parameterized import org.junit.runners.Parameterized
@ -87,9 +89,10 @@ class ClientRPCInfrastructureTests : AbstractRPCTest() {
// Does nothing, doesn't throw. // Does nothing, doesn't throw.
proxy.void() proxy.void()
assertEquals("Barf!", assertFailsWith<IllegalArgumentException> { assertThatThrownBy { proxy.barf() }
proxy.barf() .isInstanceOf(CordaRuntimeException::class.java)
}.message) .hasMessage("java.lang.IllegalArgumentException: Barf!")
assertEquals("hi 5", proxy.someCalculation("hi", 5)) assertEquals("hi 5", proxy.someCalculation("hi", 5))
} }

View File

@ -10,7 +10,7 @@
package net.corda.client.rpc package net.corda.client.rpc
import com.esotericsoftware.kryo.KryoException import net.corda.core.CordaRuntimeException
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.openFuture
import net.corda.core.messaging.* import net.corda.core.messaging.*
@ -58,23 +58,29 @@ class RPCFailureTests {
@Test @Test
fun `kotlin NPE`() = rpc { fun `kotlin NPE`() = rpc {
assertThatThrownBy { it.kotlinNPE() }.isInstanceOf(KotlinNullPointerException::class.java) assertThatThrownBy { it.kotlinNPE() }.isInstanceOf(CordaRuntimeException::class.java)
.hasMessageContaining("kotlin.KotlinNullPointerException")
} }
@Test @Test
fun `kotlin NPE async`() = rpc { fun `kotlin NPE async`() = rpc {
val future = it.kotlinNPEAsync() val future = it.kotlinNPEAsync()
assertThatThrownBy { future.getOrThrow() }.isInstanceOf(KotlinNullPointerException::class.java) assertThatThrownBy { future.getOrThrow() }.isInstanceOf(CordaRuntimeException::class.java)
.hasMessageContaining("kotlin.KotlinNullPointerException")
} }
@Test @Test
fun unserializable() = rpc { fun `unserializable`() = rpc {
assertThatThrownBy { it.getUnserializable() }.isInstanceOf(KryoException::class.java) assertThatThrownBy { it.getUnserializable() }.isInstanceOf(CordaRuntimeException::class.java)
.hasMessageContaining("java.io.NotSerializableException:")
.hasMessageContaining("Unserializable is not on the whitelist or annotated with @CordaSerializable.")
} }
@Test @Test
fun `unserializable async`() = rpc { fun `unserializable async`() = rpc {
val future = it.getUnserializableAsync() val future = it.getUnserializableAsync()
assertThatThrownBy { future.getOrThrow() }.isInstanceOf(KryoException::class.java) assertThatThrownBy { future.getOrThrow() }.isInstanceOf(CordaRuntimeException::class.java)
.hasMessageContaining("java.io.NotSerializableException:")
.hasMessageContaining("Unserializable is not on the whitelist or annotated with @CordaSerializable.")
} }
} }

View File

@ -10,6 +10,7 @@
package net.corda.core.concurrent package net.corda.core.concurrent
import net.corda.core.serialization.CordaSerializable
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future import java.util.concurrent.Future
@ -17,6 +18,7 @@ import java.util.concurrent.Future
* Same as [Future] with additional methods to provide some of the features of [java.util.concurrent.CompletableFuture] while minimising the API surface area. * Same as [Future] with additional methods to provide some of the features of [java.util.concurrent.CompletableFuture] while minimising the API surface area.
* In Kotlin, to avoid compile errors, whenever CordaFuture is used in a parameter or extension method receiver type, its type parameter should be specified with out variance. * In Kotlin, to avoid compile errors, whenever CordaFuture is used in a parameter or extension method receiver type, its type parameter should be specified with out variance.
*/ */
@CordaSerializable
interface CordaFuture<V> : Future<V> { interface CordaFuture<V> : Future<V> {
/** /**
* Run the given callback when this future is done, on the completion thread. * Run the given callback when this future is done, on the completion thread.

View File

@ -20,6 +20,7 @@ import rx.Observable
* [FlowHandle] is a serialisable handle for the started flow, parameterised by the type of the flow's return value. * [FlowHandle] is a serialisable handle for the started flow, parameterised by the type of the flow's return value.
*/ */
@DoNotImplement @DoNotImplement
@CordaSerializable
interface FlowHandle<A> : AutoCloseable { interface FlowHandle<A> : AutoCloseable {
/** /**
* The started state machine's ID. * The started state machine's ID.

View File

@ -124,11 +124,7 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
} }
/** /**
* FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleState] * FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleStates]
*
* Valid TokenType implementations defined by Amount<T> are
* [Currency] as used in [Cash] contract state
* [Commodity] as used in [CommodityContract] state
*/ */
data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null, data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
val owner: List<AbstractParty>? = null, val owner: List<AbstractParty>? = null,
@ -150,8 +146,6 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
* *
* Params * Params
* [expression] refers to a (composable) type safe [CriteriaExpression] * [expression] refers to a (composable) type safe [CriteriaExpression]
*
* Refer to [CommercialPaper.State] for a concrete example.
*/ */
data class VaultCustomQueryCriteria<L : PersistentState> @JvmOverloads constructor data class VaultCustomQueryCriteria<L : PersistentState> @JvmOverloads constructor
(val expression: CriteriaExpression<L, Boolean>, (val expression: CriteriaExpression<L, Boolean>,

View File

@ -195,6 +195,15 @@ interface SerializationContext {
enum class UseCase { P2P, RPCServer, RPCClient, Storage, Checkpoint, Testing } enum class UseCase { P2P, RPCServer, RPCClient, Storage, Checkpoint, Testing }
} }
/**
* Set of well known properties that may be set on a serialization context. This doesn't preclude
* others being set that aren't keyed on this enumeration, but for general use properties adding a
* well known key here is preferred.
*/
enum class ContextPropertyKeys {
SERIALIZERS
}
/** /**
* Global singletons to be used as defaults that are injected elsewhere (generally, in the node or in RPC client). * Global singletons to be used as defaults that are injected elsewhere (generally, in the node or in RPC client).
*/ */

View File

@ -16,7 +16,7 @@ package net.corda.core.serialization
* a proxy serializer can be written that extends this type whose purpose is to move between those an * a proxy serializer can be written that extends this type whose purpose is to move between those an
* unserializable types and an intermediate representation. * unserializable types and an intermediate representation.
* *
* NOTE: The proxy object should be specified as a seperate class. However, this can be defined within the * NOTE: The proxy object should be specified as a separate class. However, this can be defined within the
* scope of the custom serializer. * scope of the custom serializer.
*/ */
interface SerializationCustomSerializer<OBJ, PROXY> { interface SerializationCustomSerializer<OBJ, PROXY> {

View File

@ -12,16 +12,20 @@ package net.corda.core.utilities
import com.esotericsoftware.kryo.KryoException import com.esotericsoftware.kryo.KryoException
import net.corda.core.crypto.random63BitValue import net.corda.core.crypto.random63BitValue
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.*
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.serialization.KRYO_CHECKPOINT_CONTEXT import net.corda.nodeapi.internal.serialization.KRYO_CHECKPOINT_CONTEXT
import net.corda.nodeapi.internal.serialization.SerializationContextImpl
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.SerializationEnvironmentRule
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.ExpectedException import org.junit.rules.ExpectedException
object EmptyWhitelist : ClassWhitelist {
override fun hasListed(type: Class<*>): Boolean = false
}
class KotlinUtilsTest { class KotlinUtilsTest {
@Rule @Rule
@JvmField @JvmField
@ -30,6 +34,14 @@ class KotlinUtilsTest {
@Rule @Rule
val expectedEx: ExpectedException = ExpectedException.none() val expectedEx: ExpectedException = ExpectedException.none()
val KRYO_CHECKPOINT_NOWHITELIST_CONTEXT = SerializationContextImpl(kryoMagic,
SerializationDefaults.javaClass.classLoader,
EmptyWhitelist,
emptyMap(),
true,
SerializationContext.UseCase.Checkpoint,
null)
@Test @Test
fun `transient property which is null`() { fun `transient property which is null`() {
val test = NullTransientProperty() val test = NullTransientProperty()
@ -53,7 +65,8 @@ class KotlinUtilsTest {
expectedEx.expect(KryoException::class.java) expectedEx.expect(KryoException::class.java)
expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization") expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization")
val original = NonCapturingTransientProperty() val original = NonCapturingTransientProperty()
original.serialize(context = KRYO_CHECKPOINT_CONTEXT.withEncoding(null)).deserialize() original.serialize(context = KRYO_CHECKPOINT_CONTEXT.withEncoding(null))
.deserialize(context = KRYO_CHECKPOINT_NOWHITELIST_CONTEXT)
} }
@Test @Test
@ -71,8 +84,10 @@ class KotlinUtilsTest {
fun `deserialise transient property with capturing lambda`() { fun `deserialise transient property with capturing lambda`() {
expectedEx.expect(KryoException::class.java) expectedEx.expect(KryoException::class.java)
expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization") expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization")
val original = CapturingTransientProperty("Hello") val original = CapturingTransientProperty("Hello")
original.serialize(context = KRYO_CHECKPOINT_CONTEXT.withEncoding(null)).deserialize() original.serialize(context = KRYO_CHECKPOINT_CONTEXT.withEncoding(null))
.deserialize(context = KRYO_CHECKPOINT_NOWHITELIST_CONTEXT)
} }
private class NullTransientProperty { private class NullTransientProperty {

View File

@ -6,6 +6,10 @@ release, see :doc:`upgrade-notes`.
Unreleased Unreleased
========== ==========
* RPC Framework moved from Kryo to the Corda AMQP implementation [Corda-847]. This completes the removal
of ``Kryo`` from general use within Corda, remaining only for use in flow checkpointing.
* Set co.paralleluniverse.fibers.verifyInstrumentation=true in devMode. * Set co.paralleluniverse.fibers.verifyInstrumentation=true in devMode.
* Node will now gracefully fail to start if one of the required ports is already in use. * Node will now gracefully fail to start if one of the required ports is already in use.

View File

@ -238,6 +238,13 @@ absolute path to the node's base directory.
.. note:: This is temporary feature for onboarding network participants that limits their visibility for privacy reasons. .. note:: This is temporary feature for onboarding network participants that limits their visibility for privacy reasons.
:tlsCertCrlDistPoint: CRL distribution point (i.e. URL) for the TLS certificate. Default value is NULL, which indicates no CRL availability for the TLS certificate.
Note: If crlCheckSoftFail is FALSE (meaning that there is the strict CRL checking mode) this value needs to be set.
:tlsCertCrlIssuer: CRL issuer (given in the X500 name format) for the TLS certificate. Default value is NULL,
which indicates that the issuer of the TLS certificate is also the issuer of the CRL.
Note: If this parameter is set then the tlsCertCrlDistPoint needs to be set as well.
Examples Examples
-------- --------

View File

@ -52,9 +52,6 @@ There are two main steps to implementing scheduled events:
.. note:: The scheduler's clock always operates in the UTC time zone for uniformity, so any time zone logic must be .. note:: The scheduler's clock always operates in the UTC time zone for uniformity, so any time zone logic must be
performed by the contract, using ``ZonedDateTime``. performed by the contract, using ``ZonedDateTime``.
In the short term, until we have automatic flow session set up, you will also likely need to install a network
handler to help with obtaining a unique and secure random session. An example is described below.
The production and consumption of ``ContractStates`` is observed by the scheduler and the activities associated with The production and consumption of ``ContractStates`` is observed by the scheduler and the activities associated with
any consumed states are unscheduled. Any newly produced states are then queried via the ``nextScheduledActivity`` any consumed states are unscheduled. Any newly produced states are then queried via the ``nextScheduledActivity``
method and if they do not return ``null`` then that activity is scheduled based on the content of the method and if they do not return ``null`` then that activity is scheduled based on the content of the
@ -82,11 +79,3 @@ should become available and schedules an activity at that time to work out what
business process and to take on those roles. That ``FlowLogic`` will be handed the ``StateRef`` for the interest business process and to take on those roles. That ``FlowLogic`` will be handed the ``StateRef`` for the interest
rate swap ``State`` in question, as well as a tolerance ``Duration`` of how long to wait after the activity is triggered rate swap ``State`` in question, as well as a tolerance ``Duration`` of how long to wait after the activity is triggered
for the interest rate before indicating an error. for the interest rate before indicating an error.
.. note:: This is a way to create a reference to the FlowLogic class and its constructor parameters to instantiate.
As previously mentioned, we currently need a small network handler to assist with session setup until the work to
automate that is complete. See the interest rate swap specific implementation ``FixingSessionInitiationHandler`` which
is responsible for starting a ``FlowLogic`` to perform one role in the fixing flow with the ``sessionID`` sent
by the ``FixingRoleDecider`` on the other node which then launches the other role in the fixing flow. Currently
the handler needs to be manually installed in the node.

View File

@ -105,11 +105,9 @@ The current set of network parameters:
:notaries: List of identity and validation type (either validating or non-validating) of the notaries which are permitted :notaries: List of identity and validation type (either validating or non-validating) of the notaries which are permitted
in the compatibility zone. in the compatibility zone.
:maxMessageSize: (This is currently ignored. However, it will be wired up in a future release.) :maxMessageSize: Maximum allowed size in bytes of an individual message sent over the wire. Note that attachments are
a special case and may be fragmented for streaming transfer, however, an individual transaction or flow message
.. TODO Replace the above with this once wired: Maximum allowed size in bytes of an individual message sent over the wire. Note that attachments are may not be larger than this value.
a special case and may be fragmented for streaming transfer, however, an individual transaction or flow message
may not be larger than this value.
:maxTransactionSize: Maximum allowed size in bytes of a transaction. This is the size of the transaction object and its attachments. :maxTransactionSize: Maximum allowed size in bytes of a transaction. This is the size of the transaction object and its attachments.

View File

@ -76,6 +76,11 @@ dependencies {
testCompile "junit:junit:$junit_version" testCompile "junit:junit:$junit_version"
testCompile "org.assertj:assertj-core:$assertj_version" testCompile "org.assertj:assertj-core:$assertj_version"
testCompile project(':node-driver') testCompile project(':node-driver')
compile ("org.apache.activemq:artemis-amqp-protocol:${artemis_version}") {
// Gains our proton-j version from core module.
exclude group: 'org.apache.qpid', module: 'proton-j'
}
} }
configurations { configurations {

View File

@ -28,7 +28,9 @@ class InternalNodeException(message: String) : CordaRuntimeException(message) {
(wrapped as? CordaRuntimeException)?.setCause(null) (wrapped as? CordaRuntimeException)?.setCause(null)
return when { return when {
whitelisted.any { it.isInstance(wrapped) } -> wrapped whitelisted.any { it.isInstance(wrapped) } -> wrapped
else -> InternalNodeException(DEFAULT_MESSAGE) else -> InternalNodeException(DEFAULT_MESSAGE).apply {
stackTrace = emptyArray()
}
} }
} }
} }

View File

@ -56,6 +56,7 @@ class ArtemisMessagingClient(
minLargeMessageSize = maxMessageSize minLargeMessageSize = maxMessageSize
isUseGlobalPools = nodeSerializationEnv != null isUseGlobalPools = nodeSerializationEnv != null
confirmationWindowSize = this@ArtemisMessagingClient.confirmationWindowSize confirmationWindowSize = this@ArtemisMessagingClient.confirmationWindowSize
addIncomingInterceptor(ArtemisMessageSizeChecksInterceptor(maxMessageSize))
} }
val sessionFactory = locator.createSessionFactory() val sessionFactory = locator.createSessionFactory()
// Login using the node username. The broker will authenticate us as its node (as opposed to another peer) // Login using the node username. The broker will authenticate us as its node (as opposed to another peer)

View File

@ -40,7 +40,9 @@ class ArtemisMessagingComponent {
const val BRIDGE_CONTROL = "${INTERNAL_PREFIX}bridge.control" const val BRIDGE_CONTROL = "${INTERNAL_PREFIX}bridge.control"
const val BRIDGE_NOTIFY = "${INTERNAL_PREFIX}bridge.notify" const val BRIDGE_NOTIFY = "${INTERNAL_PREFIX}bridge.notify"
const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications" const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications"
// 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 { object P2PMessagingHeaders {
// This is a "property" attached to an Artemis MQ message object, which contains our own notion of "topic". // 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 // We should probably try to unify our notion of "topic" (really, just a string that identifies an endpoint

View File

@ -22,3 +22,7 @@ import java.nio.file.Path
fun Path.requireOnDefaultFileSystem() { fun Path.requireOnDefaultFileSystem() {
require(fileSystem == FileSystems.getDefault()) { "Artemis only uses the default file system" } require(fileSystem == FileSystems.getDefault()) { "Artemis only uses the default file system" }
} }
fun requireMessageSize(messageSize: Int, limit: Int) {
require(messageSize <= limit) { "Message exceeds maxMessageSize network parameter, maxMessageSize: [$limit], message size: [$messageSize]" }
}

View File

@ -0,0 +1,50 @@
package net.corda.nodeapi.internal
import net.corda.core.utilities.contextLogger
import org.apache.activemq.artemis.api.core.BaseInterceptor
import org.apache.activemq.artemis.api.core.Interceptor
import org.apache.activemq.artemis.core.protocol.core.Packet
import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.MessagePacket
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessage
import org.apache.activemq.artemis.protocol.amqp.broker.AmqpInterceptor
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection
class ArtemisMessageSizeChecksInterceptor(maxMessageSize: Int) : MessageSizeChecksInterceptor<Packet>(maxMessageSize), Interceptor {
override fun getMessageSize(packet: Packet?): Int? {
return when (packet) {
// This is an estimate of how much memory a Message body takes up.
// Note, it is only an estimate
is MessagePacket -> (packet.message.persistentSize - packet.message.headersAndPropertiesEncodeSize - 4).toInt()
// Skip all artemis control messages.
else -> null
}
}
}
class AmqpMessageSizeChecksInterceptor(maxMessageSize: Int) : MessageSizeChecksInterceptor<AMQPMessage>(maxMessageSize), AmqpInterceptor {
override fun getMessageSize(packet: AMQPMessage?): Int? = packet?.length
}
/**
* Artemis message interceptor to enforce maxMessageSize on incoming messages.
*/
sealed class MessageSizeChecksInterceptor<T : Any>(private val maxMessageSize: Int) : BaseInterceptor<T> {
companion object {
private val logger = contextLogger()
}
override fun intercept(packet: T, connection: RemotingConnection?): Boolean {
val messageSize = getMessageSize(packet) ?: return true
return if (messageSize > maxMessageSize) {
logger.warn("Message size exceeds maxMessageSize network parameter, maxMessageSize: [$maxMessageSize], message size: [$messageSize], " +
"dropping message, client id :${connection?.clientID}")
false
} else {
true
}
}
// get size of the message in byte, returns null if the message is null or size don't need to be checked.
abstract fun getMessageSize(packet: T?): Int?
}

View File

@ -48,7 +48,8 @@ import kotlin.concurrent.withLock
* The Netty thread pool used by the AMQPBridges is also shared and managed by the AMQPBridgeManager. * The Netty thread pool used by the AMQPBridges is also shared and managed by the AMQPBridgeManager.
*/ */
@VisibleForTesting @VisibleForTesting
class AMQPBridgeManager(config: NodeSSLConfiguration, private val socksProxyConfig: SocksProxyConfig? = null, private val artemisMessageClientFactory: () -> ArtemisSessionProvider) : BridgeManager { class AMQPBridgeManager(config: NodeSSLConfiguration, private val socksProxyConfig: SocksProxyConfig? = null,
private val maxMessageSize: Int, private val artemisMessageClientFactory: () -> ArtemisSessionProvider) : BridgeManager {
private val lock = ReentrantLock() private val lock = ReentrantLock()
private val bridgeNameToBridgeMap = mutableMapOf<String, AMQPBridge>() private val bridgeNameToBridgeMap = mutableMapOf<String, AMQPBridge>()
@ -59,7 +60,8 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, private val socksProxyConf
private var artemis: ArtemisSessionProvider? = null private var artemis: ArtemisSessionProvider? = null
private val crlCheckSoftFail: Boolean = config.crlCheckSoftFail private val crlCheckSoftFail: Boolean = config.crlCheckSoftFail
constructor(config: NodeSSLConfiguration, p2pAddress: NetworkHostAndPort, maxMessageSize: Int, socksProxyConfig: SocksProxyConfig? = null) : this(config, socksProxyConfig, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) }) constructor(config: NodeSSLConfiguration, p2pAddress: NetworkHostAndPort, maxMessageSize: Int, socksProxyConfig: SocksProxyConfig? = null) : this(config, socksProxyConfig, maxMessageSize, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) })
companion object { companion object {
private const val NUM_BRIDGE_THREADS = 0 // Default sized pool private const val NUM_BRIDGE_THREADS = 0 // Default sized pool
@ -82,14 +84,16 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, private val socksProxyConf
crlCheckSoftFail: Boolean, crlCheckSoftFail: Boolean,
sharedEventGroup: EventLoopGroup, sharedEventGroup: EventLoopGroup,
socksProxyConfig: SocksProxyConfig?, socksProxyConfig: SocksProxyConfig?,
private val artemis: ArtemisSessionProvider) { private val artemis: ArtemisSessionProvider,
private val maxMessageSize: Int) {
companion object { companion object {
fun getBridgeName(queueName: String, hostAndPort: NetworkHostAndPort): String = "$queueName -> $hostAndPort" fun getBridgeName(queueName: String, hostAndPort: NetworkHostAndPort): String = "$queueName -> $hostAndPort"
} }
private val log = LoggerFactory.getLogger("$bridgeName:${legalNames.first()}") private val log = LoggerFactory.getLogger("$bridgeName:${legalNames.first()}")
val amqpClient = AMQPClient(listOf(target), legalNames, PEER_USER, PEER_USER, keyStore, keyStorePrivateKeyPassword, trustStore, crlCheckSoftFail, sharedThreadPool = sharedEventGroup, socksProxyConfig = socksProxyConfig) val amqpClient = AMQPClient(listOf(target), legalNames, PEER_USER, PEER_USER, keyStore, keyStorePrivateKeyPassword, trustStore, crlCheckSoftFail,
sharedThreadPool = sharedEventGroup, socksProxyConfig = socksProxyConfig, maxMessageSize = maxMessageSize)
val bridgeName: String get() = getBridgeName(queueName, target) val bridgeName: String get() = getBridgeName(queueName, target)
private val lock = ReentrantLock() // lock to serialise session level access private val lock = ReentrantLock() // lock to serialise session level access
private var session: ClientSession? = null private var session: ClientSession? = null
@ -141,6 +145,13 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, private val socksProxyConf
} }
private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) { private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) {
if (artemisMessage.bodySize > maxMessageSize) {
log.warn("Message exceeds maxMessageSize network parameter, maxMessageSize: [$maxMessageSize], message size: [${artemisMessage.bodySize}], " +
"dropping message, uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}")
// Ack the message to prevent same message being sent to us again.
artemisMessage.acknowledge()
return
}
val data = ByteArray(artemisMessage.bodySize).apply { artemisMessage.bodyBuffer.readBytes(this) } val data = ByteArray(artemisMessage.bodySize).apply { artemisMessage.bodyBuffer.readBytes(this) }
val properties = HashMap<String, Any?>() val properties = HashMap<String, Any?>()
for (key in P2PMessagingHeaders.whitelistedHeaders) { for (key in P2PMessagingHeaders.whitelistedHeaders) {
@ -183,7 +194,9 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, private val socksProxyConf
if (bridgeExists(getBridgeName(queueName, target))) { if (bridgeExists(getBridgeName(queueName, target))) {
return return
} }
val newBridge = AMQPBridge(queueName, target, legalNames, keyStore, keyStorePrivateKeyPassword, trustStore, crlCheckSoftFail, sharedEventLoopGroup!!, socksProxyConfig, artemis!!)
val newBridge = AMQPBridge(queueName, target, legalNames, keyStore, keyStorePrivateKeyPassword, trustStore, crlCheckSoftFail, sharedEventLoopGroup!!, socksProxyConfig, artemis!!, maxMessageSize)
lock.withLock { lock.withLock {
bridgeNameToBridgeMap[newBridge.bridgeName] = newBridge bridgeNameToBridgeMap[newBridge.bridgeName] = newBridge
} }

View File

@ -33,19 +33,24 @@ import rx.subjects.PublishSubject
import java.util.* import java.util.*
class BridgeControlListener(val config: NodeSSLConfiguration, class BridgeControlListener(val config: NodeSSLConfiguration,
socksProxyConfig: SocksProxyConfig? = null, socksProxyConfig: SocksProxyConfig? = null,
maxMessageSize: Int,
val artemisMessageClientFactory: () -> ArtemisSessionProvider) : AutoCloseable { val artemisMessageClientFactory: () -> ArtemisSessionProvider) : AutoCloseable {
private val bridgeId: String = UUID.randomUUID().toString() private val bridgeId: String = UUID.randomUUID().toString()
private val bridgeControlQueue = "$BRIDGE_CONTROL.$bridgeId" private val bridgeControlQueue = "$BRIDGE_CONTROL.$bridgeId"
private val bridgeManager: BridgeManager = AMQPBridgeManager(config, socksProxyConfig, artemisMessageClientFactory) private val bridgeManager: BridgeManager = AMQPBridgeManager(config, socksProxyConfig, maxMessageSize,
artemisMessageClientFactory)
private val validInboundQueues = mutableSetOf<String>() private val validInboundQueues = mutableSetOf<String>()
private var artemis: ArtemisSessionProvider? = null private var artemis: ArtemisSessionProvider? = null
private var controlConsumer: ClientConsumer? = null private var controlConsumer: ClientConsumer? = null
constructor(config: NodeSSLConfiguration, constructor(config: NodeSSLConfiguration,
p2pAddress: NetworkHostAndPort, p2pAddress: NetworkHostAndPort,
maxMessageSize: Int, maxMessageSize: Int,
socksProxy: SocksProxyConfig? = null) : this(config, socksProxy, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) }) socksProxy: SocksProxyConfig? = null) : this(config, socksProxy, maxMessageSize, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) })
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()

View File

@ -33,10 +33,11 @@ import net.corda.nodeapi.internal.ContractsJarFile
import net.corda.nodeapi.internal.DEV_ROOT_CA import net.corda.nodeapi.internal.DEV_ROOT_CA
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier.Companion.NODE_INFO_FILE_NAME_PREFIX import net.corda.nodeapi.internal.network.NodeInfoFilesCopier.Companion.NODE_INFO_FILE_NAME_PREFIX
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme import net.corda.nodeapi.internal.serialization.amqp.AbstractAMQPSerializationScheme
import net.corda.nodeapi.internal.serialization.amqp.amqpMagic
import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
import java.nio.file.Path import java.nio.file.Path
@ -288,7 +289,7 @@ class NetworkBootstrapper {
_contextSerializationEnv.set(SerializationEnvironmentImpl( _contextSerializationEnv.set(SerializationEnvironmentImpl(
SerializationFactoryImpl().apply { SerializationFactoryImpl().apply {
registerScheme(KryoParametersSerializationScheme) registerScheme(KryoParametersSerializationScheme)
registerScheme(AMQPServerSerializationScheme()) registerScheme(AMQPParametersSerializationScheme)
}, },
AMQP_P2P_CONTEXT) AMQP_P2P_CONTEXT)
) )
@ -302,4 +303,13 @@ class NetworkBootstrapper {
override fun rpcClientKryoPool(context: SerializationContext) = throw UnsupportedOperationException() override fun rpcClientKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
override fun rpcServerKryoPool(context: SerializationContext) = throw UnsupportedOperationException() override fun rpcServerKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
} }
private object AMQPParametersSerializationScheme : AbstractAMQPSerializationScheme(emptyList()) {
override fun rpcClientSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException()
override fun rpcServerSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException()
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean {
return magic == amqpMagic && target == SerializationContext.UseCase.P2P
}
}
} }

View File

@ -362,7 +362,7 @@ internal class ConnectionStateMachine(serverMode: Boolean,
val connection = event.connection val connection = event.connection
val channel = connection?.context as? Channel val channel = connection?.context as? Channel
if (channel != null) { if (channel != null) {
val appProperties = HashMap(amqpMessage.applicationProperties.value as Map<String, Any?>) val appProperties = HashMap(amqpMessage.applicationProperties.value)
appProperties["_AMQ_VALIDATED_USER"] = remoteLegalName appProperties["_AMQ_VALIDATED_USER"] = remoteLegalName
val localAddress = channel.localAddress() as InetSocketAddress val localAddress = channel.localAddress() as InetSocketAddress
val remoteAddress = channel.remoteAddress() as InetSocketAddress val remoteAddress = channel.remoteAddress() as InetSocketAddress

View File

@ -27,6 +27,7 @@ import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
import net.corda.nodeapi.internal.protonwrapper.messages.SendableMessage import net.corda.nodeapi.internal.protonwrapper.messages.SendableMessage
import net.corda.nodeapi.internal.protonwrapper.messages.impl.SendableMessageImpl import net.corda.nodeapi.internal.protonwrapper.messages.impl.SendableMessageImpl
import net.corda.nodeapi.internal.requireMessageSize
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import java.net.InetSocketAddress import java.net.InetSocketAddress
@ -68,7 +69,8 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
private val crlCheckSoftFail: Boolean, private val crlCheckSoftFail: Boolean,
private val trace: Boolean = false, private val trace: Boolean = false,
private val sharedThreadPool: EventLoopGroup? = null, private val sharedThreadPool: EventLoopGroup? = null,
private val socksProxyConfig: SocksProxyConfig? = null) : AutoCloseable { private val socksProxyConfig: SocksProxyConfig? = null,
private val maxMessageSize: Int) : AutoCloseable {
companion object { companion object {
init { init {
InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE) InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE)
@ -118,17 +120,15 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
} }
} }
private val closeListener = object : ChannelFutureListener { private val closeListener = ChannelFutureListener { future ->
override fun operationComplete(future: ChannelFuture) { log.info("Disconnected from $currentTarget")
log.info("Disconnected from $currentTarget") future.channel()?.disconnect()
future.channel()?.disconnect() clientChannel = null
clientChannel = null if (!stopping) {
if (!stopping) { workerGroup?.schedule({
workerGroup?.schedule({ nextTarget()
nextTarget() restart()
restart() }, retryInterval, TimeUnit.MILLISECONDS)
}, retryInterval, TimeUnit.MILLISECONDS)
}
} }
} }
@ -228,6 +228,7 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
topic: String, topic: String,
destinationLegalName: String, destinationLegalName: String,
properties: Map<String, Any?>): SendableMessage { properties: Map<String, Any?>): SendableMessage {
requireMessageSize(payload.size, maxMessageSize)
return SendableMessageImpl(payload, topic, destinationLegalName, currentTarget, properties) return SendableMessageImpl(payload, topic, destinationLegalName, currentTarget, properties)
} }

View File

@ -27,6 +27,7 @@ import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
import net.corda.nodeapi.internal.protonwrapper.messages.SendableMessage import net.corda.nodeapi.internal.protonwrapper.messages.SendableMessage
import net.corda.nodeapi.internal.protonwrapper.messages.impl.SendableMessageImpl import net.corda.nodeapi.internal.protonwrapper.messages.impl.SendableMessageImpl
import net.corda.nodeapi.internal.requireMessageSize
import org.apache.qpid.proton.engine.Delivery import org.apache.qpid.proton.engine.Delivery
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
@ -51,7 +52,8 @@ class AMQPServer(val hostName: String,
private val keyStorePrivateKeyPassword: CharArray, private val keyStorePrivateKeyPassword: CharArray,
private val trustStore: KeyStore, private val trustStore: KeyStore,
private val crlCheckSoftFail: Boolean, private val crlCheckSoftFail: Boolean,
private val trace: Boolean = false) : AutoCloseable { private val trace: Boolean = false,
private val maxMessageSize: Int) : AutoCloseable {
companion object { companion object {
init { init {
@ -78,7 +80,8 @@ class AMQPServer(val hostName: String,
keyStorePrivateKeyPassword: String, keyStorePrivateKeyPassword: String,
trustStore: KeyStore, trustStore: KeyStore,
crlCheckSoftFail: Boolean, crlCheckSoftFail: Boolean,
trace: Boolean = false) : this(hostName, port, userName, password, keyStore, keyStorePrivateKeyPassword.toCharArray(), trustStore, crlCheckSoftFail, trace) trace: Boolean = false,
maxMessageSize: Int) : this(hostName, port, userName, password, keyStore, keyStorePrivateKeyPassword.toCharArray(), trustStore, crlCheckSoftFail, trace, maxMessageSize)
private class ServerChannelInitializer(val parent: AMQPServer) : ChannelInitializer<SocketChannel>() { private class ServerChannelInitializer(val parent: AMQPServer) : ChannelInitializer<SocketChannel>() {
private val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) private val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
@ -166,6 +169,7 @@ class AMQPServer(val hostName: String,
destinationLegalName: String, destinationLegalName: String,
destinationLink: NetworkHostAndPort, destinationLink: NetworkHostAndPort,
properties: Map<String, Any?>): SendableMessage { properties: Map<String, Any?>): SendableMessage {
requireMessageSize(payload.size, maxMessageSize)
val dest = InetSocketAddress(destinationLink.host, destinationLink.port) val dest = InetSocketAddress(destinationLink.host, destinationLink.port)
require(dest in clientChannels.keys) { require(dest in clientChannels.keys) {
"Destination not available" "Destination not available"

View File

@ -23,13 +23,7 @@ import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
* servers from trying to instantiate any of them. * servers from trying to instantiate any of them.
*/ */
val KRYO_RPC_CLIENT_CONTEXT = SerializationContextImpl(kryoMagic,
SerializationDefaults.javaClass.classLoader,
GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()),
emptyMap(),
true,
SerializationContext.UseCase.RPCClient,
null)
val AMQP_RPC_CLIENT_CONTEXT = SerializationContextImpl(amqpMagic, val AMQP_RPC_CLIENT_CONTEXT = SerializationContextImpl(amqpMagic,
SerializationDefaults.javaClass.classLoader, SerializationDefaults.javaClass.classLoader,
GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()),

View File

@ -124,8 +124,9 @@ open class SerializationFactoryImpl(
val lookupKey = magic to target val lookupKey = magic to target
return schemes.computeIfAbsent(lookupKey) { return schemes.computeIfAbsent(lookupKey) {
registeredSchemes.filter { it.canDeserializeVersion(magic, target) }.forEach { return@computeIfAbsent it } // XXX: Not single? registeredSchemes.filter { it.canDeserializeVersion(magic, target) }.forEach { return@computeIfAbsent it } // XXX: Not single?
logger.warn("Cannot find serialization scheme for: $lookupKey, registeredSchemes are: $registeredSchemes") logger.warn("Cannot find serialization scheme for: [$lookupKey, " +
throw UnsupportedOperationException("Serialization scheme not supported.") "${if (magic == amqpMagic) "AMQP" else if (magic == kryoMagic) "Kryo" else "UNKNOWN MAGIC"}] registeredSchemes are: $registeredSchemes")
throw UnsupportedOperationException("Serialization scheme $lookupKey not supported.")
} to magic } to magic
} }

View File

@ -27,13 +27,6 @@ import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
* MUST be kept separate! * MUST be kept separate!
*/ */
val KRYO_RPC_SERVER_CONTEXT = SerializationContextImpl(kryoMagic,
SerializationDefaults.javaClass.classLoader,
GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()),
emptyMap(),
true,
SerializationContext.UseCase.RPCServer,
null)
val AMQP_STORAGE_CONTEXT = SerializationContextImpl(amqpMagic, val AMQP_STORAGE_CONTEXT = SerializationContextImpl(amqpMagic,
SerializationDefaults.javaClass.classLoader, SerializationDefaults.javaClass.classLoader,

View File

@ -15,6 +15,7 @@ package net.corda.nodeapi.internal.serialization.amqp
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.Cordapp
import net.corda.core.internal.objectOrNewInstance import net.corda.core.internal.objectOrNewInstance
import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.* import net.corda.core.serialization.*
import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.ByteSequence
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
@ -22,7 +23,6 @@ import net.corda.nodeapi.internal.serialization.DefaultWhitelist
import net.corda.nodeapi.internal.serialization.MutableClassWhitelist import net.corda.nodeapi.internal.serialization.MutableClassWhitelist
import net.corda.nodeapi.internal.serialization.SerializationScheme import net.corda.nodeapi.internal.serialization.SerializationScheme
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
import java.security.PublicKey
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@ -128,6 +128,12 @@ abstract class AbstractAMQPSerializationScheme(
factory.registerExternal(CorDappCustomSerializer(customSerializer, factory)) factory.registerExternal(CorDappCustomSerializer(customSerializer, factory))
} }
} }
context.properties[ContextPropertyKeys.SERIALIZERS]?.apply {
uncheckedCast<Any, List<CustomSerializer<out Any>>>(this).forEach {
factory.register(it)
}
}
} }
/* /*
@ -141,7 +147,9 @@ abstract class AbstractAMQPSerializationScheme(
protected abstract fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory protected abstract fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory
protected abstract fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory protected abstract fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory
protected open val publicKeySerializer: CustomSerializer.Implements<PublicKey> = net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer
// Not used as a simple direct import to facilitate testing
open val publicKeySerializer : CustomSerializer<*> = net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer
private fun getSerializerFactory(context: SerializationContext): SerializerFactory { private fun getSerializerFactory(context: SerializationContext): SerializerFactory {
return serializerFactoriesForContexts.computeIfAbsent(Pair(context.whitelist, context.deserializationClassLoader)) { return serializerFactoriesForContexts.computeIfAbsent(Pair(context.whitelist, context.deserializationClassLoader)) {
@ -172,52 +180,3 @@ abstract class AbstractAMQPSerializationScheme(
protected fun canDeserializeVersion(magic: CordaSerializationMagic) = magic == amqpMagic protected fun canDeserializeVersion(magic: CordaSerializationMagic) = magic == amqpMagic
} }
// TODO: This will eventually cover server RPC as well and move to node module, but for now this is not implemented
class AMQPServerSerializationScheme(
cordappCustomSerializers: Set<SerializationCustomSerializer<*, *>>,
serializerFactoriesForContexts: MutableMap<Pair<ClassWhitelist, ClassLoader>, SerializerFactory>
) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) {
constructor(cordapps: List<Cordapp>) : this(cordapps.customSerializers, ConcurrentHashMap())
constructor() : this(emptySet(), ConcurrentHashMap())
override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory {
throw UnsupportedOperationException()
}
override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory {
throw UnsupportedOperationException()
}
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean {
return canDeserializeVersion(magic) &&
(target == SerializationContext.UseCase.P2P || target == SerializationContext.UseCase.Storage)
}
}
// TODO: This will eventually cover client RPC as well and move to client module, but for now this is not implemented
class AMQPClientSerializationScheme(
cordappCustomSerializers: Set<SerializationCustomSerializer<*,*>>,
serializerFactoriesForContexts: MutableMap<Pair<ClassWhitelist, ClassLoader>, SerializerFactory>
) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) {
constructor(cordapps: List<Cordapp>) : this(cordapps.customSerializers, ConcurrentHashMap())
constructor() : this(emptySet(), ConcurrentHashMap())
override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory {
throw UnsupportedOperationException()
}
override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory {
throw UnsupportedOperationException()
}
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean {
return canDeserializeVersion(magic) &&
(target == SerializationContext.UseCase.P2P || target == SerializationContext.UseCase.Storage)
}
}

View File

@ -295,7 +295,11 @@ open class SerializerFactory(
} }
private fun makeClassSerializer(clazz: Class<*>, type: Type, declaredType: Type): AMQPSerializer<Any> = serializersByType.computeIfAbsent(type) { private fun makeClassSerializer(clazz: Class<*>, type: Type, declaredType: Type): AMQPSerializer<Any> = serializersByType.computeIfAbsent(type) {
if (isPrimitive(clazz)) { if (clazz.isSynthetic) {
// Explicitly ban synthetic classes, we have no way of recreating them when deserializing. This also
// captures Lambda expressions and other anonymous functions
throw NotSerializableException(type.typeName)
} else if (isPrimitive(clazz)) {
AMQPPrimitiveSerializer(clazz) AMQPPrimitiveSerializer(clazz)
} else { } else {
findCustomSerializer(clazz, declaredType) ?: run { findCustomSerializer(clazz, declaredType) ?: run {

View File

@ -12,7 +12,6 @@ package net.corda.nodeapi.internal.serialization.amqp.custom
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
import net.corda.nodeapi.internal.serialization.amqp.custom.ClassSerializer.ClassProxy
/** /**
* A serializer for [Class] that uses [ClassProxy] proxy object to write out * A serializer for [Class] that uses [ClassProxy] proxy object to write out

View File

@ -49,7 +49,7 @@ object InputStreamSerializer : CustomSerializer.Implements<InputStream>(InputStr
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
context: SerializationContext context: SerializationContext
): InputStream { ) : InputStream {
val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
return bits.inputStream() return bits.inputStream()
} }

View File

@ -0,0 +1,28 @@
package net.corda.nodeapi.internal.serialization.amqp.custom
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
import rx.Notification
class RxNotificationSerializer(
factory: SerializerFactory
) : CustomSerializer.Proxy<rx.Notification<*>, RxNotificationSerializer.Proxy>(
Notification::class.java,
Proxy::class.java,
factory
) {
data class Proxy(
val kind: Notification.Kind,
val t: Throwable?,
val value: Any?)
override fun toProxy(obj: Notification<*>) = Proxy(obj.kind, obj.throwable, obj.value)
override fun fromProxy(proxy: Proxy): Notification<*> {
return when (proxy.kind) {
Notification.Kind.OnCompleted -> Notification.createOnCompleted<Any>()
Notification.Kind.OnError -> Notification.createOnError<Any>(proxy.t)
Notification.Kind.OnNext -> Notification.createOnNext(proxy.value)
}
}
}

View File

@ -17,7 +17,6 @@ import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer
import com.esotericsoftware.kryo.serializers.FieldSerializer import com.esotericsoftware.kryo.serializers.FieldSerializer
import com.esotericsoftware.kryo.util.MapReferenceResolver import com.esotericsoftware.kryo.util.MapReferenceResolver
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.PrivacySalt import net.corda.core.contracts.PrivacySalt
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
@ -28,8 +27,6 @@ import net.corda.core.serialization.SerializationContext.UseCase.Checkpoint
import net.corda.core.serialization.SerializationContext.UseCase.Storage import net.corda.core.serialization.SerializationContext.UseCase.Storage
import net.corda.core.serialization.SerializeAsTokenContext import net.corda.core.serialization.SerializeAsTokenContext
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.core.toFuture
import net.corda.core.toObservable
import net.corda.core.transactions.* import net.corda.core.transactions.*
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509CertificateFactory
@ -38,7 +35,6 @@ import net.corda.nodeapi.internal.serialization.CordaClassResolver
import net.corda.nodeapi.internal.serialization.serializationContextKey import net.corda.nodeapi.internal.serialization.serializationContextKey
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import rx.Observable
import java.io.InputStream import java.io.InputStream
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.security.PrivateKey import java.security.PrivateKey
@ -57,39 +53,16 @@ import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaType import kotlin.reflect.jvm.javaType
/** /**
* Serialization utilities, using the Kryo framework with a custom serialiser for immutable data classes and a dead * Serialization utilities, using the Kryo framework with a custom serializer for immutable data classes and a dead
* simple, totally non-extensible binary (sub)format. * simple, totally non-extensible binary (sub)format. Used exclusively within Corda for checkpointing flows as
* * it will happily deserialise literally anything, including malicious streams that would reconstruct classes
* This is NOT what should be used in any final platform product, rather, the final state should be a precisely * in invalid states and thus violating system invariants. In the context of checkpointing a Java stack, this is
* specified and standardised binary format with attention paid to anti-malleability, versioning and performance. * absolutely the functionality we desire, for a stable binary wire format and persistence technology, we have
* FIX SBE is a potential candidate: it prioritises performance over convenience and was designed for HFT. Google * the AMQP implementation.
* Protocol Buffers with a minor tightening to make field reordering illegal is another possibility.
*
* FIX SBE:
* https://real-logic.github.io/simple-binary-encoding/
* http://mechanical-sympathy.blogspot.co.at/2014/05/simple-binary-encoding.html
* Protocol buffers:
* https://developers.google.com/protocol-buffers/
*
* But for now we use Kryo to maximise prototyping speed.
*
* Note that this code ignores *ALL* concerns beyond convenience, in particular it ignores:
*
* - Performance
* - Security
*
* This code will happily deserialise literally anything, including malicious streams that would reconstruct classes
* in invalid states, thus violating system invariants. It isn't designed to handle malicious streams and therefore,
* isn't usable beyond the prototyping stage. But that's fine: we can revisit serialisation technologies later after
* a formal evaluation process.
*
* We now distinguish between internal, storage related Kryo and external, network facing Kryo. We presently use
* some non-whitelisted classes as part of internal storage.
* TODO: eliminate internal, storage related whitelist issues, such as private keys in blob storage.
*/ */
/** /**
* A serialiser that avoids writing the wrapper class to the byte stream, thus ensuring [SerializedBytes] is a pure * A serializer that avoids writing the wrapper class to the byte stream, thus ensuring [SerializedBytes] is a pure
* type safety hack. * type safety hack.
*/ */
object SerializedBytesSerializer : Serializer<SerializedBytes<Any>>() { object SerializedBytesSerializer : Serializer<SerializedBytes<Any>>() {
@ -405,44 +378,6 @@ open class CordaKryo(classResolver: ClassResolver) : Kryo(classResolver, MapRefe
} }
} }
/**
* The Kryo used for the RPC wire protocol.
*/
// Every type in the wire protocol is listed here explicitly.
// This is annoying to write out, but will make it easier to formalise the wire protocol when the time comes,
// because we can see everything we're using in one place.
class RPCKryo(observableSerializer: Serializer<Observable<*>>, serializationContext: SerializationContext) : CordaKryo(CordaClassResolver(serializationContext)) {
init {
DefaultKryoCustomizer.customize(this)
// RPC specific classes
register(InputStream::class.java, InputStreamSerializer)
register(Observable::class.java, observableSerializer)
register(CordaFuture::class,
read = { kryo, input -> observableSerializer.read(kryo, input, Observable::class.java).toFuture() },
write = { kryo, output, obj -> observableSerializer.write(kryo, output, obj.toObservable()) }
)
}
override fun getRegistration(type: Class<*>): Registration {
if (Observable::class.java != type && Observable::class.java.isAssignableFrom(type)) {
return super.getRegistration(Observable::class.java)
}
if (InputStream::class.java != type && InputStream::class.java.isAssignableFrom(type)) {
return super.getRegistration(InputStream::class.java)
}
if (CordaFuture::class.java != type && CordaFuture::class.java.isAssignableFrom(type)) {
return super.getRegistration(CordaFuture::class.java)
}
type.requireExternal("RPC not allowed to deserialise internal classes")
return super.getRegistration(type)
}
private fun Class<*>.requireExternal(msg: String) {
require(!name.startsWith("net.corda.node.") && ".internal" !in name) { "$msg: $name" }
}
}
inline fun <T : Any> Kryo.register( inline fun <T : Any> Kryo.register(
type: KClass<T>, type: KClass<T>,
crossinline read: (Kryo, Input) -> T, crossinline read: (Kryo, Input) -> T,

View File

@ -14,13 +14,13 @@ import com.google.common.collect.Maps;
import net.corda.core.serialization.SerializationContext; import net.corda.core.serialization.SerializationContext;
import net.corda.core.serialization.SerializationFactory; import net.corda.core.serialization.SerializationFactory;
import net.corda.core.serialization.SerializedBytes; import net.corda.core.serialization.SerializedBytes;
import net.corda.nodeapi.internal.serialization.kryo.CordaClosureBlacklistSerializer; import net.corda.nodeapi.internal.serialization.amqp.SchemaKt;
import net.corda.nodeapi.internal.serialization.kryo.KryoSerializationSchemeKt;
import net.corda.testing.core.SerializationEnvironmentRule; import net.corda.testing.core.SerializationEnvironmentRule;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import java.io.NotSerializableException;
import java.io.Serializable; import java.io.Serializable;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
@ -43,20 +43,17 @@ public final class ForbiddenLambdaSerializationTests {
@Test @Test
public final void serialization_fails_for_serializable_java_lambdas() { public final void serialization_fails_for_serializable_java_lambdas() {
contexts.forEach(ctx -> { contexts.forEach(ctx -> {
SerializationContext context = new SerializationContextImpl(KryoSerializationSchemeKt.getKryoMagic(), SerializationContext context = new SerializationContextImpl(SchemaKt.getAmqpMagic(),
this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, ctx, null); this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, ctx, null);
String value = "Hey"; String value = "Hey";
Callable<String> target = (Callable<String> & Serializable) () -> value; Callable<String> target = (Callable<String> & Serializable) () -> value;
Throwable throwable = catchThrowable(() -> serialize(target, context)); Throwable throwable = catchThrowable(() -> serialize(target, context));
assertThat(throwable).isNotNull(); assertThat(throwable)
assertThat(throwable).isInstanceOf(IllegalArgumentException.class); .isNotNull()
if (ctx != SerializationContext.UseCase.RPCServer && ctx != SerializationContext.UseCase.Storage) { .isInstanceOf(NotSerializableException.class)
assertThat(throwable).hasMessage(CordaClosureBlacklistSerializer.ERROR_MESSAGE); .hasMessageContaining(getClass().getName());
} else {
assertThat(throwable).hasMessageContaining("RPC not allowed to deserialise internal classes");
}
}); });
} }
@ -64,21 +61,17 @@ public final class ForbiddenLambdaSerializationTests {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public final void serialization_fails_for_not_serializable_java_lambdas() { public final void serialization_fails_for_not_serializable_java_lambdas() {
contexts.forEach(ctx -> { contexts.forEach(ctx -> {
SerializationContext context = new SerializationContextImpl(KryoSerializationSchemeKt.getKryoMagic(), SerializationContext context = new SerializationContextImpl(SchemaKt.getAmqpMagic(),
this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, ctx, null); this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, ctx, null);
String value = "Hey"; String value = "Hey";
Callable<String> target = () -> value; Callable<String> target = () -> value;
Throwable throwable = catchThrowable(() -> serialize(target, context)); Throwable throwable = catchThrowable(() -> serialize(target, context));
assertThat(throwable).isNotNull(); assertThat(throwable)
assertThat(throwable).isInstanceOf(IllegalArgumentException.class); .isNotNull()
assertThat(throwable).isInstanceOf(IllegalArgumentException.class); .isInstanceOf(NotSerializableException.class)
if (ctx != SerializationContext.UseCase.RPCServer && ctx != SerializationContext.UseCase.Storage) { .hasMessageContaining(getClass().getName());
assertThat(throwable).hasMessage(CordaClosureBlacklistSerializer.ERROR_MESSAGE);
} else {
assertThat(throwable).hasMessageContaining("RPC not allowed to deserialise internal classes");
}
}); });
} }

View File

@ -18,13 +18,13 @@ import net.corda.core.internal.div
import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.node.serialization.kryo.KryoServerSerializationScheme import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.createDevKeyStores import net.corda.nodeapi.internal.createDevKeyStores
import net.corda.nodeapi.internal.serialization.AllWhitelist import net.corda.nodeapi.internal.serialization.AllWhitelist
import net.corda.nodeapi.internal.serialization.SerializationContextImpl import net.corda.nodeapi.internal.serialization.SerializationContextImpl
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic import net.corda.nodeapi.internal.serialization.amqp.amqpMagic
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.TestIdentity import net.corda.testing.core.TestIdentity
@ -345,8 +345,8 @@ class X509UtilitiesTest {
@Test @Test
fun `serialize - deserialize X509Certififcate`() { fun `serialize - deserialize X509Certififcate`() {
val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) } val factory = SerializationFactoryImpl().apply { registerScheme(AMQPServerSerializationScheme()) }
val context = SerializationContextImpl(kryoMagic, val context = SerializationContextImpl(amqpMagic,
javaClass.classLoader, javaClass.classLoader,
AllWhitelist, AllWhitelist,
emptyMap(), emptyMap(),
@ -361,8 +361,8 @@ class X509UtilitiesTest {
@Test @Test
fun `serialize - deserialize X509CertPath`() { fun `serialize - deserialize X509CertPath`() {
val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) } val factory = SerializationFactoryImpl().apply { registerScheme(AMQPServerSerializationScheme()) }
val context = SerializationContextImpl(kryoMagic, val context = SerializationContextImpl(amqpMagic,
javaClass.classLoader, javaClass.classLoader,
AllWhitelist, AllWhitelist,
emptyMap(), emptyMap(),

View File

@ -40,6 +40,7 @@ import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationFactory import net.corda.core.serialization.SerializationFactory
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
import net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA import net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA
import net.corda.nodeapi.internal.crypto.ContentSignerBuilder import net.corda.nodeapi.internal.crypto.ContentSignerBuilder
import net.corda.nodeapi.internal.serialization.AllWhitelist import net.corda.nodeapi.internal.serialization.AllWhitelist

View File

@ -15,6 +15,7 @@ import com.esotericsoftware.kryo.KryoException
import com.esotericsoftware.kryo.KryoSerializable import com.esotericsoftware.kryo.KryoSerializable
import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.pool.KryoPool
import com.google.common.primitives.Ints import com.google.common.primitives.Ints
import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever import com.nhaarman.mockito_kotlin.whenever
@ -22,9 +23,9 @@ import net.corda.core.contracts.PrivacySalt
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.internal.FetchDataFlow import net.corda.core.internal.FetchDataFlow
import net.corda.core.serialization.* import net.corda.core.serialization.*
import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.sequence import net.corda.core.utilities.sequence
import net.corda.node.serialization.kryo.KryoServerSerializationScheme
import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.nodeapi.internal.serialization.* import net.corda.nodeapi.internal.serialization.*
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
@ -44,6 +45,17 @@ import java.time.Instant
import java.util.* import java.util.*
import kotlin.test.* import kotlin.test.*
class TestScheme : AbstractKryoSerializationScheme() {
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean {
return magic == kryoMagic && target != SerializationContext.UseCase.RPCClient
}
override fun rpcClientKryoPool(context: SerializationContext): KryoPool = throw UnsupportedOperationException()
override fun rpcServerKryoPool(context: SerializationContext): KryoPool = throw UnsupportedOperationException()
}
@RunWith(Parameterized::class) @RunWith(Parameterized::class)
class KryoTests(private val compression: CordaSerializationEncoding?) { class KryoTests(private val compression: CordaSerializationEncoding?) {
companion object { companion object {
@ -58,7 +70,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) {
@Before @Before
fun setup() { fun setup() {
factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) } factory = SerializationFactoryImpl().apply { registerScheme(TestScheme()) }
context = SerializationContextImpl(kryoMagic, context = SerializationContextImpl(kryoMagic,
javaClass.classLoader, javaClass.classLoader,
AllWhitelist, AllWhitelist,
@ -86,11 +98,12 @@ class KryoTests(private val compression: CordaSerializationEncoding?) {
assertThat(bits.deserialize(factory, context)).isEqualTo(Person("bob", null)) assertThat(bits.deserialize(factory, context)).isEqualTo(Person("bob", null))
} }
@Test @Test
fun `serialised form is stable when the same object instance is added to the deserialised object graph`() { fun `serialised form is stable when the same object instance is added to the deserialised object graph`() {
val noReferencesContext = context.withoutReferences() val noReferencesContext = context.withoutReferences()
val obj = Ints.toByteArray(0x01234567).sequence() val obj : ByteSequence = Ints.toByteArray(0x01234567).sequence()
val originalList = arrayListOf(obj) val originalList : ArrayList<ByteSequence> = arrayListOf(obj)
val deserialisedList = originalList.serialize(factory, noReferencesContext).deserialize(factory, noReferencesContext) val deserialisedList = originalList.serialize(factory, noReferencesContext).deserialize(factory, noReferencesContext)
originalList += obj originalList += obj
deserialisedList += obj deserialisedList += obj
@ -278,7 +291,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) {
} }
} }
Tmp() Tmp()
val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) } val factory = SerializationFactoryImpl().apply { registerScheme(TestScheme()) }
val context = SerializationContextImpl(kryoMagic, val context = SerializationContextImpl(kryoMagic,
javaClass.classLoader, javaClass.classLoader,
AllWhitelist, AllWhitelist,

View File

@ -12,6 +12,7 @@ package net.corda.node
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.core.CordaRuntimeException
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.div import net.corda.core.internal.div
@ -21,6 +22,7 @@ import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.NodeStartup import net.corda.node.internal.NodeStartup
import net.corda.node.services.Permissions.Companion.startFlow import net.corda.node.services.Permissions.Companion.startFlow
import net.corda.nodeapi.exceptions.InternalNodeException
import net.corda.testing.common.internal.ProjectStructure.projectRootDir import net.corda.testing.common.internal.ProjectStructure.projectRootDir
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME import net.corda.testing.core.BOB_NAME
@ -51,8 +53,11 @@ class BootTests : IntegrationTest() {
fun `java deserialization is disabled`() { fun `java deserialization is disabled`() {
driver { driver {
val user = User("u", "p", setOf(startFlow<ObjectInputStreamFlow>())) val user = User("u", "p", setOf(startFlow<ObjectInputStreamFlow>()))
val future = CordaRPCClient(startNode(rpcUsers = listOf(user)).getOrThrow().rpcAddress).start(user.username, user.password).proxy.startFlow(::ObjectInputStreamFlow).returnValue val future = CordaRPCClient(startNode(rpcUsers = listOf(user)).getOrThrow().rpcAddress).
assertThatThrownBy { future.getOrThrow() }.isInstanceOf(InvalidClassException::class.java).hasMessage("filter status: REJECTED") start(user.username, user.password).proxy.startFlow(::ObjectInputStreamFlow).returnValue
assertThatThrownBy { future.getOrThrow() }
.isInstanceOf(CordaRuntimeException::class.java)
.hasMessageContaining(InternalNodeException.defaultMessage())
} }
} }

View File

@ -272,6 +272,7 @@ class AMQPBridgeTest {
return Triple(artemisServer, artemisClient, bridgeManager) return Triple(artemisServer, artemisClient, bridgeManager)
} }
private fun createArtemisReceiver(targetAdress: NetworkHostAndPort, workingDir: String): Pair<ArtemisMessagingServer, ArtemisMessagingClient> { private fun createArtemisReceiver(targetAdress: NetworkHostAndPort, workingDir: String): Pair<ArtemisMessagingServer, ArtemisMessagingClient> {
val artemisConfig = rigorousMock<AbstractNodeConfiguration>().also { val artemisConfig = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath() / workingDir).whenever(it).baseDirectory doReturn(temporaryFolder.root.toPath() / workingDir).whenever(it).baseDirectory
@ -293,8 +294,7 @@ class AMQPBridgeTest {
} }
private fun createAMQPServer(maxMessageSize: Int = MAX_MESSAGE_SIZE): AMQPServer {
private fun createAMQPServer(): AMQPServer {
val serverConfig = rigorousMock<AbstractNodeConfiguration>().also { val serverConfig = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory
doReturn(BOB_NAME).whenever(it).myLegalName doReturn(BOB_NAME).whenever(it).myLegalName
@ -311,7 +311,8 @@ class AMQPBridgeTest {
serverConfig.keyStorePassword, serverConfig.keyStorePassword,
serverConfig.loadTrustStore().internal, serverConfig.loadTrustStore().internal,
crlCheckSoftFail = true, crlCheckSoftFail = true,
trace = true trace = true,
maxMessageSize = maxMessageSize
) )
} }
} }

View File

@ -19,10 +19,7 @@ import net.corda.nodeapi.internal.crypto.*
import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPServer import net.corda.nodeapi.internal.protonwrapper.netty.AMQPServer
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.*
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.CHARLIE_NAME
import net.corda.testing.core.freePort
import net.corda.testing.internal.DEV_INTERMEDIATE_CA import net.corda.testing.internal.DEV_INTERMEDIATE_CA
import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.internal.DEV_ROOT_CA
import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.rigorousMock
@ -272,7 +269,8 @@ class CertificateRevocationListNodeTests {
private fun createClient(targetPort: Int, private fun createClient(targetPort: Int,
crlCheckSoftFail: Boolean, crlCheckSoftFail: Boolean,
nodeCrlDistPoint: String = "http://${server.hostAndPort}/crl/node.crl", nodeCrlDistPoint: String = "http://${server.hostAndPort}/crl/node.crl",
tlsCrlDistPoint: String? = "http://${server.hostAndPort}/crl/empty.crl"): Pair<AMQPClient, X509Certificate> { tlsCrlDistPoint: String? = "http://${server.hostAndPort}/crl/empty.crl",
maxMessageSize: Int = MAX_MESSAGE_SIZE): Pair<AMQPClient, X509Certificate> {
val clientConfig = rigorousMock<AbstractNodeConfiguration>().also { val clientConfig = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath() / "client").whenever(it).baseDirectory doReturn(temporaryFolder.root.toPath() / "client").whenever(it).baseDirectory
doReturn(BOB_NAME).whenever(it).myLegalName doReturn(BOB_NAME).whenever(it).myLegalName
@ -292,13 +290,15 @@ class CertificateRevocationListNodeTests {
clientKeystore, clientKeystore,
clientConfig.keyStorePassword, clientConfig.keyStorePassword,
clientTruststore, clientTruststore,
crlCheckSoftFail), nodeCert) crlCheckSoftFail,
maxMessageSize = maxMessageSize), nodeCert)
} }
private fun createServer(port: Int, name: CordaX500Name = ALICE_NAME, private fun createServer(port: Int, name: CordaX500Name = ALICE_NAME,
crlCheckSoftFail: Boolean, crlCheckSoftFail: Boolean,
nodeCrlDistPoint: String = "http://${server.hostAndPort}/crl/node.crl", nodeCrlDistPoint: String = "http://${server.hostAndPort}/crl/node.crl",
tlsCrlDistPoint: String? = "http://${server.hostAndPort}/crl/empty.crl"): Pair<AMQPServer, X509Certificate> { tlsCrlDistPoint: String? = "http://${server.hostAndPort}/crl/empty.crl",
maxMessageSize: Int = MAX_MESSAGE_SIZE): Pair<AMQPServer, X509Certificate> {
val serverConfig = rigorousMock<AbstractNodeConfiguration>().also { val serverConfig = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory
doReturn(name).whenever(it).myLegalName doReturn(name).whenever(it).myLegalName
@ -318,7 +318,8 @@ class CertificateRevocationListNodeTests {
serverKeystore, serverKeystore,
serverConfig.keyStorePassword, serverConfig.keyStorePassword,
serverTruststore, serverTruststore,
crlCheckSoftFail), nodeCert) crlCheckSoftFail,
maxMessageSize = maxMessageSize), nodeCert)
} }
private fun SSLConfiguration.recreateNodeCaAndTlsCertificates(nodeCaCrlDistPoint: String, tlsCrlDistPoint: String?): X509Certificate { private fun SSLConfiguration.recreateNodeCaAndTlsCertificates(nodeCaCrlDistPoint: String, tlsCrlDistPoint: String?): X509Certificate {

View File

@ -34,6 +34,7 @@ import net.corda.testing.core.*
import net.corda.testing.internal.createDevIntermediateCaCertPath import net.corda.testing.internal.createDevIntermediateCaCertPath
import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.rigorousMock
import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.RoutingType
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertArrayEquals
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -91,7 +92,8 @@ class ProtonWrapperTests {
@Test @Test
fun `AMPQ Client fails to connect when crl soft fail check is disabled`() { fun `AMPQ Client fails to connect when crl soft fail check is disabled`() {
val amqpServer = createServer(serverPort, CordaX500Name("Rogue 1", "London", "GB"), false) val amqpServer = createServer(serverPort, CordaX500Name("Rogue 1", "London", "GB"),
maxMessageSize = MAX_MESSAGE_SIZE, crlCheckSoftFail = false)
amqpServer.use { amqpServer.use {
amqpServer.start() amqpServer.start()
val amqpClient = createClient() val amqpClient = createClient()
@ -283,6 +285,52 @@ class ProtonWrapperTests {
server.stop() server.stop()
} }
@Test
fun `Send a message larger then maxMessageSize from AMQP to Artemis inbox`() {
val maxMessageSize = 100_000
val (server, artemisClient) = createArtemisServerAndClient(maxMessageSize)
val amqpClient = createClient(maxMessageSize)
val clientConnected = amqpClient.onConnection.toFuture()
amqpClient.start()
assertEquals(true, clientConnected.get().connected)
assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal))
val artemis = artemisClient.started!!
val sendAddress = P2P_PREFIX + "Test"
artemis.session.createQueue(sendAddress, RoutingType.ANYCAST, "queue", true)
val consumer = artemis.session.createConsumer("queue")
val testProperty = mutableMapOf<String, Any?>()
testProperty["TestProp"] = "1"
// Send normal message.
val testData = ByteArray(maxMessageSize)
val message = amqpClient.createMessage(testData, sendAddress, CHARLIE_NAME.toString(), testProperty)
amqpClient.write(message)
assertEquals(MessageStatus.Acknowledged, message.onComplete.get())
val received = consumer.receive()
assertEquals("1", received.getStringProperty("TestProp"))
assertArrayEquals(testData, ByteArray(received.bodySize).apply { received.bodyBuffer.readBytes(this) })
// Send message larger then max message size.
val largeData = ByteArray(maxMessageSize + 1)
// Create message will fail.
assertThatThrownBy {
amqpClient.createMessage(largeData, sendAddress, CHARLIE_NAME.toString(), testProperty)
}.hasMessageContaining("Message exceeds maxMessageSize network parameter")
// Send normal message again to confirm the large message didn't reach the server and client is not killed by the message.
val message2 = amqpClient.createMessage(testData, sendAddress, CHARLIE_NAME.toString(), testProperty)
amqpClient.write(message2)
assertEquals(MessageStatus.Acknowledged, message2.onComplete.get())
val received2 = consumer.receive()
assertEquals("1", received2.getStringProperty("TestProp"))
assertArrayEquals(testData, ByteArray(received2.bodySize).apply { received2.bodyBuffer.readBytes(this) })
amqpClient.stop()
artemisClient.stop()
server.stop()
}
@Test @Test
fun `shared AMQPClient threadpool tests`() { fun `shared AMQPClient threadpool tests`() {
val amqpServer = createServer(serverPort) val amqpServer = createServer(serverPort)
@ -333,7 +381,7 @@ class ProtonWrapperTests {
} }
} }
private fun createArtemisServerAndClient(): Pair<ArtemisMessagingServer, ArtemisMessagingClient> { private fun createArtemisServerAndClient(maxMessageSize: Int = MAX_MESSAGE_SIZE): Pair<ArtemisMessagingServer, ArtemisMessagingClient> {
val artemisConfig = rigorousMock<AbstractNodeConfiguration>().also { val artemisConfig = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath() / "artemis").whenever(it).baseDirectory doReturn(temporaryFolder.root.toPath() / "artemis").whenever(it).baseDirectory
doReturn(CHARLIE_NAME).whenever(it).myLegalName doReturn(CHARLIE_NAME).whenever(it).myLegalName
@ -347,14 +395,14 @@ class ProtonWrapperTests {
} }
artemisConfig.configureWithDevSSLCertificate() artemisConfig.configureWithDevSSLCertificate()
val server = ArtemisMessagingServer(artemisConfig, NetworkHostAndPort("0.0.0.0", artemisPort), MAX_MESSAGE_SIZE) val server = ArtemisMessagingServer(artemisConfig, NetworkHostAndPort("0.0.0.0", artemisPort), maxMessageSize)
val client = ArtemisMessagingClient(artemisConfig, NetworkHostAndPort("localhost", artemisPort), MAX_MESSAGE_SIZE) val client = ArtemisMessagingClient(artemisConfig, NetworkHostAndPort("localhost", artemisPort), maxMessageSize)
server.start() server.start()
client.start() client.start()
return Pair(server, client) return Pair(server, client)
} }
private fun createClient(): AMQPClient { private fun createClient(maxMessageSize: Int = MAX_MESSAGE_SIZE): AMQPClient {
val clientConfig = rigorousMock<AbstractNodeConfiguration>().also { val clientConfig = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath() / "client").whenever(it).baseDirectory doReturn(temporaryFolder.root.toPath() / "client").whenever(it).baseDirectory
doReturn(BOB_NAME).whenever(it).myLegalName doReturn(BOB_NAME).whenever(it).myLegalName
@ -376,10 +424,11 @@ class ProtonWrapperTests {
clientKeystore, clientKeystore,
clientConfig.keyStorePassword, clientConfig.keyStorePassword,
clientTruststore, clientTruststore,
true) true,
maxMessageSize = maxMessageSize)
} }
private fun createSharedThreadsClient(sharedEventGroup: EventLoopGroup, id: Int): AMQPClient { private fun createSharedThreadsClient(sharedEventGroup: EventLoopGroup, id: Int, maxMessageSize: Int = MAX_MESSAGE_SIZE): AMQPClient {
val clientConfig = rigorousMock<AbstractNodeConfiguration>().also { val clientConfig = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath() / "client_%$id").whenever(it).baseDirectory doReturn(temporaryFolder.root.toPath() / "client_%$id").whenever(it).baseDirectory
doReturn(CordaX500Name(null, "client $id", "Corda", "London", null, "GB")).whenever(it).myLegalName doReturn(CordaX500Name(null, "client $id", "Corda", "London", null, "GB")).whenever(it).myLegalName
@ -400,10 +449,14 @@ class ProtonWrapperTests {
clientConfig.keyStorePassword, clientConfig.keyStorePassword,
clientTruststore, clientTruststore,
true, true,
sharedThreadPool = sharedEventGroup) sharedThreadPool = sharedEventGroup,
maxMessageSize = maxMessageSize)
} }
private fun createServer(port: Int, name: CordaX500Name = ALICE_NAME, crlCheckSoftFail: Boolean = true): AMQPServer {
private fun createServer(port: Int, name: CordaX500Name = ALICE_NAME, maxMessageSize: Int = MAX_MESSAGE_SIZE,
crlCheckSoftFail: Boolean = true
): AMQPServer {
val serverConfig = rigorousMock<AbstractNodeConfiguration>().also { val serverConfig = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory
doReturn(name).whenever(it).myLegalName doReturn(name).whenever(it).myLegalName
@ -423,6 +476,7 @@ class ProtonWrapperTests {
serverKeystore, serverKeystore,
serverConfig.keyStorePassword, serverConfig.keyStorePassword,
serverTruststore, serverTruststore,
crlCheckSoftFail = true) crlCheckSoftFail = true,
maxMessageSize = maxMessageSize)
} }
} }

View File

@ -45,12 +45,12 @@ class RpcExceptionHandlingTest : IntegrationTest() {
val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow() val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow()
assertThatCode { node.rpc.startFlow(::Flow).returnValue.getOrThrow() } assertThatCode { node.rpc.startFlow(::Flow).returnValue.getOrThrow() }.isInstanceOfSatisfying(InternalNodeException::class.java) { exception ->
.isInstanceOfSatisfying(InternalNodeException::class.java) { exception ->
assertThat(exception).hasNoCause() assertThat(exception).hasNoCause()
assertThat(exception.stackTrace).isEmpty() assertThat(exception.stackTrace).isEmpty()
assertThat(exception.message).isEqualTo(InternalNodeException.defaultMessage()) assertThat(exception.message).isEqualTo(InternalNodeException.defaultMessage())
} }
} }
} }
@ -61,12 +61,12 @@ class RpcExceptionHandlingTest : IntegrationTest() {
val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow() val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow()
val clientRelevantMessage = "This is for the players!" val clientRelevantMessage = "This is for the players!"
assertThatCode { node.rpc.startFlow(::ClientRelevantErrorFlow, clientRelevantMessage).returnValue.getOrThrow() } assertThatCode { node.rpc.startFlow(::ClientRelevantErrorFlow, clientRelevantMessage).returnValue.getOrThrow() }.isInstanceOfSatisfying(ClientRelevantException::class.java) { exception ->
.isInstanceOfSatisfying(ClientRelevantException::class.java) { exception ->
assertThat(exception).hasNoCause() assertThat(exception).hasNoCause()
assertThat(exception.stackTrace).isEmpty() assertThat(exception.stackTrace).isEmpty()
assertThat(exception.message).isEqualTo(clientRelevantMessage) assertThat(exception.message).isEqualTo(clientRelevantMessage)
} }
} }
} }
@ -93,10 +93,11 @@ class RpcExceptionHandlingTest : IntegrationTest() {
assertThatCode { nodeA.rpc.startFlow(::InitFlow, nodeB.nodeInfo.singleIdentity()).returnValue.getOrThrow() } assertThatCode { nodeA.rpc.startFlow(::InitFlow, nodeB.nodeInfo.singleIdentity()).returnValue.getOrThrow() }
.isInstanceOfSatisfying(InternalNodeException::class.java) { exception -> .isInstanceOfSatisfying(InternalNodeException::class.java) { exception ->
assertThat(exception).hasNoCause()
assertThat(exception.stackTrace).isEmpty() assertThat(exception).hasNoCause()
assertThat(exception.message).isEqualTo(InternalNodeException.defaultMessage()) assertThat(exception.stackTrace).isEmpty()
} assertThat(exception.message).isEqualTo(InternalNodeException.defaultMessage())
}
} }
} }
} }

View File

@ -11,7 +11,7 @@
package net.corda.node.internal package net.corda.node.internal
import com.codahale.metrics.JmxReporter import com.codahale.metrics.JmxReporter
import net.corda.client.rpc.internal.serialization.kryo.KryoClientSerializationScheme import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.internal.Emoji import net.corda.core.internal.Emoji
import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.openFuture
@ -36,6 +36,7 @@ import net.corda.node.internal.artemis.BrokerAddresses
import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.internal.security.RPCSecurityManagerWithAdditionalUser import net.corda.node.internal.security.RPCSecurityManagerWithAdditionalUser
import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
import net.corda.node.serialization.kryo.KryoServerSerializationScheme import net.corda.node.serialization.kryo.KryoServerSerializationScheme
import net.corda.node.services.api.NodePropertiesStore import net.corda.node.services.api.NodePropertiesStore
import net.corda.node.services.api.SchemaService import net.corda.node.services.api.SchemaService
@ -52,7 +53,6 @@ import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.bridging.BridgeControlListener import net.corda.nodeapi.internal.bridging.BridgeControlListener
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.serialization.* import net.corda.nodeapi.internal.serialization.*
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import rx.Scheduler import rx.Scheduler
@ -108,14 +108,14 @@ open class Node(configuration: NodeConfiguration,
const val scanPackagesSeparator = "," const val scanPackagesSeparator = ","
@JvmStatic @JvmStatic
protected fun makeCordappLoader(configuration: NodeConfiguration, versionInfo: VersionInfo): CordappLoader { private fun makeCordappLoader(configuration: NodeConfiguration, versionInfo: VersionInfo): CordappLoader {
return System.getProperty(scanPackagesSystemProperty)?.let { scanPackages -> return System.getProperty(scanPackagesSystemProperty)?.let { scanPackages ->
CordappLoader.createDefaultWithTestPackages(configuration, scanPackages.split(scanPackagesSeparator), versionInfo) CordappLoader.createDefaultWithTestPackages(configuration, scanPackages.split(scanPackagesSeparator), versionInfo)
} ?: CordappLoader.createDefault(configuration.baseDirectory, versionInfo) } ?: CordappLoader.createDefault(configuration.baseDirectory, versionInfo)
} }
// TODO: make this configurable.
// TODO Wire up maxMessageSize const val MAX_RPC_MESSAGE_SIZE = 10485760
const val MAX_FILE_SIZE = 10485760
} }
override val log: Logger get() = staticLog override val log: Logger get() = staticLog
@ -185,7 +185,7 @@ open class Node(configuration: NodeConfiguration,
if (!configuration.messagingServerExternal) { if (!configuration.messagingServerExternal) {
val brokerBindAddress = configuration.messagingServerAddress ?: NetworkHostAndPort("0.0.0.0", configuration.p2pAddress.port) val brokerBindAddress = configuration.messagingServerAddress ?: NetworkHostAndPort("0.0.0.0", configuration.p2pAddress.port)
messageBroker = ArtemisMessagingServer(configuration, brokerBindAddress, MAX_FILE_SIZE) messageBroker = ArtemisMessagingServer(configuration, brokerBindAddress, networkParameters.maxMessageSize)
} }
val serverAddress = configuration.messagingServerAddress ?: NetworkHostAndPort("localhost", configuration.p2pAddress.port) val serverAddress = configuration.messagingServerAddress ?: NetworkHostAndPort("localhost", configuration.p2pAddress.port)
@ -194,18 +194,21 @@ open class Node(configuration: NodeConfiguration,
} else { } else {
startLocalRpcBroker() startLocalRpcBroker()
} }
val advertisedAddress = info.addresses.single() val advertisedAddress = info.addresses.single()
val externalBridge = configuration.enterpriseConfiguration.externalBridge val externalBridge = configuration.enterpriseConfiguration.externalBridge
if (externalBridge == null || !externalBridge) { if (externalBridge == null || !externalBridge) {
bridgeControlListener = BridgeControlListener(configuration, serverAddress, /*networkParameters.maxMessageSize*/MAX_FILE_SIZE) bridgeControlListener = BridgeControlListener(configuration, serverAddress, networkParameters.maxMessageSize)
} }
printBasicNodeInfo("Advertised P2P messaging addresses", info.addresses.joinToString()) printBasicNodeInfo("Advertised P2P messaging addresses", info.addresses.joinToString())
val rpcServerConfiguration = RPCServerConfiguration.default.copy( val rpcServerConfiguration = RPCServerConfiguration.default.copy(
rpcThreadPoolSize = configuration.enterpriseConfiguration.tuning.rpcThreadPoolSize rpcThreadPoolSize = configuration.enterpriseConfiguration.tuning.rpcThreadPoolSize
) )
rpcServerAddresses?.let { rpcServerAddresses?.let {
rpcMessagingClient = RPCMessagingClient(configuration.rpcOptions.sslConfig, it.admin, /*networkParameters.maxMessageSize*/MAX_FILE_SIZE, rpcServerConfiguration) rpcMessagingClient = RPCMessagingClient(configuration.rpcOptions.sslConfig, it.admin, MAX_RPC_MESSAGE_SIZE, rpcServerConfiguration)
printBasicNodeInfo("RPC connection address", it.primary.toString()) printBasicNodeInfo("RPC connection address", it.primary.toString())
printBasicNodeInfo("RPC admin connection address", it.admin.toString()) printBasicNodeInfo("RPC admin connection address", it.admin.toString())
} }
@ -227,7 +230,7 @@ open class Node(configuration: NodeConfiguration,
services.monitoringService.metrics, services.monitoringService.metrics,
info.legalIdentities[0].name.toString(), info.legalIdentities[0].name.toString(),
advertisedAddress, advertisedAddress,
/*networkParameters.maxMessageSize*/MAX_FILE_SIZE, networkParameters.maxMessageSize,
nodeProperties.flowsDrainingMode::isEnabled, nodeProperties.flowsDrainingMode::isEnabled,
nodeProperties.flowsDrainingMode.values) nodeProperties.flowsDrainingMode.values)
} }
@ -239,9 +242,9 @@ open class Node(configuration: NodeConfiguration,
val rpcBrokerDirectory: Path = baseDirectory / "brokers" / "rpc" val rpcBrokerDirectory: Path = baseDirectory / "brokers" / "rpc"
with(rpcOptions) { with(rpcOptions) {
rpcBroker = if (useSsl) { rpcBroker = if (useSsl) {
ArtemisRpcBroker.withSsl(this.address!!, sslConfig, securityManager, certificateChainCheckPolicies, /*networkParameters.maxMessageSize*/MAX_FILE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory) ArtemisRpcBroker.withSsl(this.address!!, sslConfig, securityManager, certificateChainCheckPolicies, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory)
} else { } else {
ArtemisRpcBroker.withoutSsl(this.address!!, adminAddress!!, sslConfig, securityManager, certificateChainCheckPolicies, /*networkParameters.maxMessageSize*/MAX_FILE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory) ArtemisRpcBroker.withoutSsl(this.address!!, adminAddress!!, sslConfig, securityManager, certificateChainCheckPolicies, MAX_RPC_MESSAGE_SIZE, jmxMonitoringHttpPort != null, rpcBrokerDirectory)
} }
} }
return rpcBroker!!.addresses return rpcBroker!!.addresses
@ -412,15 +415,15 @@ open class Node(configuration: NodeConfiguration,
val classloader = cordappLoader.appClassLoader val classloader = cordappLoader.appClassLoader
nodeSerializationEnv = SerializationEnvironmentImpl( nodeSerializationEnv = SerializationEnvironmentImpl(
SerializationFactoryImpl().apply { SerializationFactoryImpl().apply {
registerScheme(KryoServerSerializationScheme())
registerScheme(AMQPServerSerializationScheme(cordappLoader.cordapps)) registerScheme(AMQPServerSerializationScheme(cordappLoader.cordapps))
registerScheme(KryoClientSerializationScheme()) registerScheme(AMQPClientSerializationScheme(cordappLoader.cordapps))
registerScheme(KryoServerSerializationScheme() )
}, },
p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader), p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader),
rpcServerContext = KRYO_RPC_SERVER_CONTEXT.withClassLoader(classloader), rpcServerContext = AMQP_RPC_SERVER_CONTEXT.withClassLoader(classloader),
storageContext = AMQP_STORAGE_CONTEXT.withClassLoader(classloader), storageContext = AMQP_STORAGE_CONTEXT.withClassLoader(classloader),
checkpointContext = KRYO_CHECKPOINT_CONTEXT.withClassLoader(classloader), checkpointContext = KRYO_CHECKPOINT_CONTEXT.withClassLoader(classloader),
rpcClientContext = if (configuration.shouldInitCrashShell()) KRYO_RPC_CLIENT_CONTEXT.withClassLoader(classloader) else null) //even Shell embeded in the node connects via RPC to the node rpcClientContext = if (configuration.shouldInitCrashShell()) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classloader) else null) //even Shell embeded in the node connects via RPC to the node
} }
private var rpcMessagingClient: RPCMessagingClient? = null private var rpcMessagingClient: RPCMessagingClient? = null

View File

@ -0,0 +1,46 @@
package net.corda.node.serialization.amqp
import net.corda.core.cordapp.Cordapp
import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationCustomSerializer
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
import net.corda.nodeapi.internal.serialization.amqp.AbstractAMQPSerializationScheme
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
import net.corda.nodeapi.internal.serialization.amqp.custom.RxNotificationSerializer
import java.util.concurrent.ConcurrentHashMap
/**
* When set as the serialization scheme, defines the RPC Server serialization scheme as using the Corda
* AMQP implementation.
*/
class AMQPServerSerializationScheme(
cordappCustomSerializers: Set<SerializationCustomSerializer<*, *>>,
serializerFactoriesForContexts: MutableMap<Pair<ClassWhitelist, ClassLoader>, SerializerFactory>
) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) {
constructor(cordapps: List<Cordapp>) : this(cordapps.customSerializers, ConcurrentHashMap())
constructor() : this(emptySet(), ConcurrentHashMap())
override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory {
throw UnsupportedOperationException()
}
override fun rpcServerSerializerFactory(context: SerializationContext) =
SerializerFactory(
context.whitelist,
context.deserializationClassLoader
).apply {
register(RpcServerObservableSerializer())
register(RpcServerCordaFutureSerializer(this))
register(RxNotificationSerializer(this))
}
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean {
return canDeserializeVersion(magic) &&
( target == SerializationContext.UseCase.P2P
|| target == SerializationContext.UseCase.Storage
|| target == SerializationContext.UseCase.RPCServer)
}
}

View File

@ -0,0 +1,35 @@
package net.corda.node.serialization.amqp
import net.corda.core.concurrent.CordaFuture
import net.corda.core.toObservable
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
import rx.Observable
import java.io.NotSerializableException
/**
* Serializer for [CordaFuture] objects where Futures are converted to Observables and
* are thus dealt with by the [RpcServerObservableSerializer]
*/
class RpcServerCordaFutureSerializer(factory: SerializerFactory)
: CustomSerializer.Proxy<CordaFuture<*>,
RpcServerCordaFutureSerializer.FutureProxy>(
CordaFuture::class.java, RpcServerCordaFutureSerializer.FutureProxy::class.java, factory
) {
override fun fromProxy(proxy: RpcServerCordaFutureSerializer.FutureProxy): CordaFuture<*> {
throw UnsupportedOperationException()
}
override fun toProxy(obj: CordaFuture<*>): RpcServerCordaFutureSerializer.FutureProxy {
try {
return FutureProxy(obj.toObservable())
} catch (e: NotSerializableException) {
throw (NotSerializableException("Failed to serialize Future as proxy Observable - ${e.message}"))
}
}
data class FutureProxy(val observable: Observable<*>)
}

View File

@ -0,0 +1,140 @@
package net.corda.node.serialization.amqp
import net.corda.core.context.Trace
import net.corda.core.serialization.SerializationContext
import net.corda.core.utilities.loggerFor
import net.corda.node.services.messaging.ObservableContextInterface
import net.corda.node.services.messaging.ObservableSubscription
import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.internal.serialization.amqp.*
import org.apache.qpid.proton.codec.Data
import rx.Notification
import rx.Observable
import rx.Subscriber
import java.io.NotSerializableException
import java.lang.reflect.Type
/**
* Server side serializer that notionally serializes RxObservables when used by the RPC
* framework for event subscriptions. Notional in the sense that the actual observable
* isn't serialized, rather a reference to the observable is, this is then used by
* the client side RPC handler to subscribe to the observable stream.
*/
class RpcServerObservableSerializer : CustomSerializer.Implements<Observable<*>>(
Observable::class.java
) {
// Would be great to make this private, but then it's so much harder to unit test
object RpcObservableContextKey
companion object {
fun createContext(
serializationContext: SerializationContext,
observableContext: ObservableContextInterface
) = serializationContext.withProperty(
RpcServerObservableSerializer.RpcObservableContextKey, observableContext)
}
override val schemaForDocumentation = Schema(
listOf(
CompositeType(
name = type.toString(),
label = "",
provides = emptyList(),
descriptor = descriptor,
fields = listOf(
Field(
name = "observableId",
type = "string",
requires = emptyList(),
default = null,
label = null,
mandatory = true,
multiple = false),
Field(
name = "observableInstant",
type = "long",
requires = emptyList(),
default = null,
label = null,
mandatory = true,
multiple = false)
))))
override fun readObject(
obj: Any, schemas: SerializationSchemas,
input: DeserializationInput,
context: SerializationContext
): Observable<*> {
throw UnsupportedOperationException()
}
override fun writeDescribedObject(
obj: Observable<*>,
data: Data,
type: Type,
output: SerializationOutput,
context: SerializationContext
) {
val observableId = Trace.InvocationId.newInstance()
if (RpcServerObservableSerializer.RpcObservableContextKey !in context.properties) {
throw NotSerializableException("Missing Observable Key on serialization context - $type")
}
val observableContext = context.properties[RpcServerObservableSerializer.RpcObservableContextKey]
as ObservableContextInterface
data.withList {
data.putString(observableId.value)
data.putLong(observableId.timestamp.toEpochMilli())
}
val observableWithSubscription = ObservableSubscription(
subscription = obj.materialize().subscribe(
object : Subscriber<Notification<*>>() {
override fun onNext(observation: Notification<*>) {
if (!isUnsubscribed) {
val message = RPCApi.ServerToClient.Observation(
id = observableId,
content = observation,
deduplicationIdentity = observableContext.deduplicationIdentity
)
observableContext.sendMessage(message)
}
}
override fun onError(exception: Throwable) {
loggerFor<RpcServerObservableSerializer>().error(
"onError called in materialize()d RPC Observable", exception)
}
override fun onCompleted() {
observableContext.clientAddressToObservables.compute(observableContext.clientAddress) { _, observables ->
if (observables != null) {
observables.remove(observableId)
if (observables.isEmpty()) {
null
} else {
observables
}
} else {
null
}
}
}
}
)
)
observableContext.clientAddressToObservables.compute(observableContext.clientAddress) { _, observables ->
if (observables == null) {
hashSetOf(observableId)
} else {
observables.add(observableId)
observables
}
}
observableContext.observableMap.put(observableId, observableWithSubscription)
}
}

View File

@ -14,22 +14,15 @@ import com.esotericsoftware.kryo.pool.KryoPool
import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationContext
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme
import net.corda.nodeapi.internal.serialization.kryo.DefaultKryoCustomizer
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
import net.corda.nodeapi.internal.serialization.kryo.RPCKryo
class KryoServerSerializationScheme : AbstractKryoSerializationScheme() { class KryoServerSerializationScheme : AbstractKryoSerializationScheme() {
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean {
return magic == kryoMagic && target != SerializationContext.UseCase.RPCClient return magic == kryoMagic && target == SerializationContext.UseCase.Checkpoint
} }
override fun rpcClientKryoPool(context: SerializationContext): KryoPool = throw UnsupportedOperationException() override fun rpcClientKryoPool(context: SerializationContext): KryoPool = throw UnsupportedOperationException()
override fun rpcServerKryoPool(context: SerializationContext): KryoPool { override fun rpcServerKryoPool(context: SerializationContext): KryoPool = throw UnsupportedOperationException()
return KryoPool.Builder {
DefaultKryoCustomizer.customize(RPCKryo(RpcServerObservableSerializer, context), publicKeySerializer).apply {
classLoader = context.deserializationClassLoader
}
}.build()
}
} }

View File

@ -1,87 +0,0 @@
package net.corda.node.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 net.corda.core.context.Trace
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationDefaults
import net.corda.node.services.messaging.ObservableSubscription
import net.corda.node.services.messaging.RPCServer
import net.corda.nodeapi.RPCApi
import org.slf4j.LoggerFactory
import rx.Notification
import rx.Observable
import rx.Subscriber
object RpcServerObservableSerializer : Serializer<Observable<*>>() {
private object RpcObservableContextKey
private val log = LoggerFactory.getLogger(javaClass)
fun createContext(observableContext: RPCServer.ObservableContext): SerializationContext {
return SerializationDefaults.RPC_SERVER_CONTEXT.withProperty(RpcServerObservableSerializer.RpcObservableContextKey, observableContext)
}
override fun read(kryo: Kryo?, input: Input?, type: Class<Observable<*>>?): Observable<Any> {
throw UnsupportedOperationException()
}
override fun write(kryo: Kryo, output: Output, observable: Observable<*>) {
val observableId = Trace.InvocationId.newInstance()
val observableContext = kryo.context[RpcObservableContextKey] as RPCServer.ObservableContext
output.writeInvocationId(observableId)
val observableWithSubscription = ObservableSubscription(
// We capture [observableContext] in the subscriber. Note that all synchronisation/kryo borrowing
// must be done again within the subscriber
subscription = observable.materialize().subscribe(
object : Subscriber<Notification<*>>() {
override fun onNext(observation: Notification<*>) {
if (!isUnsubscribed) {
val message = RPCApi.ServerToClient.Observation(
id = observableId,
content = observation,
deduplicationIdentity = observableContext.deduplicationIdentity
)
observableContext.sendMessage(message)
}
}
override fun onError(exception: Throwable) {
log.error("onError called in materialize()d RPC Observable", exception)
}
override fun onCompleted() {
observableContext.clientAddressToObservables.compute(observableContext.clientAddress) { _, observables ->
if (observables != null) {
observables.remove(observableId)
if (observables.isEmpty()) {
null
} else {
observables
}
} else {
null
}
}
}
}
)
)
observableContext.clientAddressToObservables.compute(observableContext.clientAddress) { _, observables ->
if (observables == null) {
hashSetOf(observableId)
} else {
observables.add(observableId)
observables
}
}
observableContext.observableMap.put(observableId, observableWithSubscription)
}
private fun Output.writeInvocationId(id: Trace.InvocationId) {
writeString(id.value)
writeLong(id.timestamp.toEpochMilli())
}
}

View File

@ -27,6 +27,7 @@ import net.corda.nodeapi.internal.config.parseAs
import net.corda.nodeapi.internal.persistence.CordaPersistence.DataSourceConfigTag import net.corda.nodeapi.internal.persistence.CordaPersistence.DataSourceConfigTag
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.tools.shell.SSHDConfiguration import net.corda.tools.shell.SSHDConfiguration
import org.bouncycastle.asn1.x500.X500Name
import org.slf4j.Logger import org.slf4j.Logger
import java.net.URL import java.net.URL
import java.nio.file.Path import java.nio.file.Path
@ -70,6 +71,8 @@ interface NodeConfiguration : NodeSSLConfiguration {
// do not change this value without syncing it with ScheduledFlowsDrainingModeTest // do not change this value without syncing it with ScheduledFlowsDrainingModeTest
val drainingModePollPeriod: Duration get() = Duration.ofSeconds(5) val drainingModePollPeriod: Duration get() = Duration.ofSeconds(5)
val extraNetworkMapKeys: List<UUID> val extraNetworkMapKeys: List<UUID>
val tlsCertCrlDistPoint: URL?
val tlsCertCrlIssuer: String?
fun validate(): List<String> fun validate(): List<String>
@ -190,6 +193,8 @@ data class NodeConfigurationImpl(
override val crlCheckSoftFail: Boolean, override val crlCheckSoftFail: Boolean,
override val dataSourceProperties: Properties, override val dataSourceProperties: Properties,
override val compatibilityZoneURL: URL? = null, override val compatibilityZoneURL: URL? = null,
override val tlsCertCrlDistPoint: URL? = null,
override val tlsCertCrlIssuer: String? = null,
override val rpcUsers: List<User>, override val rpcUsers: List<User>,
override val security: SecurityConfiguration? = null, override val security: SecurityConfiguration? = null,
override val verifierType: VerifierType, override val verifierType: VerifierType,
@ -241,10 +246,29 @@ data class NodeConfigurationImpl(
}.asOptions(fallbackSslOptions) }.asOptions(fallbackSslOptions)
} }
private fun validateTlsCertCrlConfig(): List<String> {
val errors = mutableListOf<String>()
if (tlsCertCrlIssuer != null) {
if (tlsCertCrlDistPoint == null) {
errors += "tlsCertCrlDistPoint needs to be specified when tlsCertCrlIssuer is not NULL"
}
try {
X500Name(tlsCertCrlIssuer)
} catch (e: Exception) {
errors += "Error when parsing tlsCertCrlIssuer: ${e.message}"
}
}
if (!crlCheckSoftFail && tlsCertCrlDistPoint == null) {
errors += "tlsCertCrlDistPoint needs to be specified when crlCheckSoftFail is FALSE"
}
return errors
}
override fun validate(): List<String> { override fun validate(): List<String> {
val errors = mutableListOf<String>() val errors = mutableListOf<String>()
errors += validateDevModeOptions() errors += validateDevModeOptions()
errors += validateRpcOptions(rpcOptions) errors += validateRpcOptions(rpcOptions)
errors += validateTlsCertCrlConfig()
return errors return errors
} }

View File

@ -29,7 +29,10 @@ import net.corda.node.services.messaging.NodeLoginModule.Companion.VERIFIER_ROLE
import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.VerifierApi import net.corda.nodeapi.VerifierApi
import net.corda.nodeapi.internal.AmqpMessageSizeChecksInterceptor
import net.corda.nodeapi.internal.ArtemisMessageSizeChecksInterceptor
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.JOURNAL_HEADER_SIZE
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
@ -79,7 +82,7 @@ import javax.security.auth.spi.LoginModule
@ThreadSafe @ThreadSafe
class ArtemisMessagingServer(private val config: NodeConfiguration, class ArtemisMessagingServer(private val config: NodeConfiguration,
private val messagingServerAddress: NetworkHostAndPort, private val messagingServerAddress: NetworkHostAndPort,
val maxMessageSize: Int) : ArtemisBroker, SingletonSerializeAsToken() { private val maxMessageSize: Int) : ArtemisBroker, SingletonSerializeAsToken() {
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()
} }
@ -127,8 +130,11 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
registerPostQueueCreationCallback { log.debug { "Queue Created: $it" } } registerPostQueueCreationCallback { log.debug { "Queue Created: $it" } }
registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } } registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } }
} }
// Config driven switch between legacy CORE bridges and the newer AMQP protocol bridges.
activeMQServer.start() activeMQServer.start()
activeMQServer.remotingService.addIncomingInterceptor(ArtemisMessageSizeChecksInterceptor(maxMessageSize))
activeMQServer.remotingService.addIncomingInterceptor(AmqpMessageSizeChecksInterceptor(maxMessageSize))
// Config driven switch between legacy CORE bridges and the newer AMQP protocol bridges.
log.info("P2P messaging server listening on $messagingServerAddress") log.info("P2P messaging server listening on $messagingServerAddress")
} }
@ -147,9 +153,9 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess
isPersistIDCache = true isPersistIDCache = true
isPopulateValidatedUser = true isPopulateValidatedUser = true
journalBufferSize_NIO = maxMessageSize // Artemis default is 490KiB - required to address IllegalArgumentException (when Artemis uses Java NIO): Record is too large to store. journalBufferSize_NIO = maxMessageSize + JOURNAL_HEADER_SIZE // Artemis default is 490KiB - required to address IllegalArgumentException (when Artemis uses Java NIO): Record is too large to store.
journalBufferSize_AIO = maxMessageSize // Required to address IllegalArgumentException (when Artemis uses Linux Async IO): Record is too large to store. journalBufferSize_AIO = maxMessageSize + JOURNAL_HEADER_SIZE // Required to address IllegalArgumentException (when Artemis uses Linux Async IO): Record is too large to store.
journalFileSize = maxMessageSize // The size of each journal file in bytes. Artemis default is 10MiB. journalFileSize = maxMessageSize + JOURNAL_HEADER_SIZE// The size of each journal file in bytes. Artemis default is 10MiB.
managementNotificationAddress = SimpleString(NOTIFICATIONS_ADDRESS) managementNotificationAddress = SimpleString(NOTIFICATIONS_ADDRESS)
connectionTtlCheckInterval = config.enterpriseConfiguration.tuning.brokerConnectionTtlCheckIntervalMs connectionTtlCheckInterval = config.enterpriseConfiguration.tuning.brokerConnectionTtlCheckIntervalMs
// JMX enablement // JMX enablement

View File

@ -0,0 +1,21 @@
package net.corda.node.services.messaging
import com.github.benmanes.caffeine.cache.Cache
import net.corda.core.context.Trace
import net.corda.nodeapi.RPCApi
import org.apache.activemq.artemis.api.core.SimpleString
import java.util.concurrent.ConcurrentHashMap
/**
* An observable context is constructed on each RPC request. If subsequently a nested Observable is encountered this
* same context is propagated by the serialization context. This way all observations rooted in a single RPC will be
* muxed correctly. Note that the context construction itself is quite cheap.
*/
interface ObservableContextInterface {
fun sendMessage(serverToClient: RPCApi.ServerToClient)
val observableMap: Cache<Trace.InvocationId, ObservableSubscription>
val clientAddressToObservables: ConcurrentHashMap<SimpleString, HashSet<Trace.InvocationId>>
val deduplicationIdentity: String
val clientAddress: SimpleString
}

View File

@ -46,6 +46,7 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisAddress import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisAddress
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_CONTROL 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.BRIDGE_NOTIFY
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.JOURNAL_HEADER_SIZE
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress
@ -55,6 +56,7 @@ import net.corda.nodeapi.internal.bridging.BridgeControl
import net.corda.nodeapi.internal.bridging.BridgeEntry import net.corda.nodeapi.internal.bridging.BridgeEntry
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
import net.corda.nodeapi.internal.requireMessageSize
import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException
import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID
import org.apache.activemq.artemis.api.core.Message.HDR_VALIDATED_USER import org.apache.activemq.artemis.api.core.Message.HDR_VALIDATED_USER
@ -218,7 +220,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
// would be the default and the two lines below can be deleted. // would be the default and the two lines below can be deleted.
connectionTTL = -1 connectionTTL = -1
clientFailureCheckPeriod = -1 clientFailureCheckPeriod = -1
minLargeMessageSize = maxMessageSize minLargeMessageSize = maxMessageSize + JOURNAL_HEADER_SIZE
isUseGlobalPools = nodeSerializationEnv != null isUseGlobalPools = nodeSerializationEnv != null
confirmationWindowSize = config.enterpriseConfiguration.tuning.p2pConfirmationWindowSize confirmationWindowSize = config.enterpriseConfiguration.tuning.p2pConfirmationWindowSize
} }
@ -289,7 +291,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
networkChangeSubscription = networkMap.changed.subscribe { updateBridgesOnNetworkChange(it) } networkChangeSubscription = networkMap.changed.subscribe { updateBridgesOnNetworkChange(it) }
} }
private fun sendBridgeControl(message: BridgeControl) { private fun sendBridgeControl(message: BridgeControl) {
state.locked { state.locked {
val controlPacket = message.serialize(context = SerializationDefaults.P2P_CONTEXT).bytes val controlPacket = message.serialize(context = SerializationDefaults.P2P_CONTEXT).bytes
val artemisMessage = producerSession!!.createMessage(false) val artemisMessage = producerSession!!.createMessage(false)
@ -397,6 +399,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
private fun artemisToCordaMessage(message: ClientMessage): ReceivedMessage? { private fun artemisToCordaMessage(message: ClientMessage): ReceivedMessage? {
try { try {
requireMessageSize(message.bodySize, maxMessageSize)
val topic = message.required(P2PMessagingHeaders.topicProperty) { getStringProperty(it) } val topic = message.required(P2PMessagingHeaders.topicProperty) { getStringProperty(it) }
val user = requireNotNull(if (externalBridge) { val user = requireNotNull(if (externalBridge) {
message.getStringProperty(P2PMessagingHeaders.bridgedCertificateSubject) ?: message.getStringProperty(HDR_VALIDATED_USER) message.getStringProperty(P2PMessagingHeaders.bridgedCertificateSubject) ?: message.getStringProperty(HDR_VALIDATED_USER)
@ -531,6 +534,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
@Suspendable @Suspendable
override fun send(message: Message, target: MessageRecipients, retryId: Long?, sequenceKey: Any) { override fun send(message: Message, target: MessageRecipients, retryId: Long?, sequenceKey: Any) {
requireMessageSize(message.data.size, maxMessageSize)
messagingExecutor!!.send(message, target) messagingExecutor!!.send(message, target)
retryId?.let { retryId?.let {
database.transaction { database.transaction {
@ -586,7 +590,8 @@ class P2PMessagingClient(val config: NodeConfiguration,
} else { } else {
// Otherwise we send the message to an internal queue for the target residing on our broker. It's then the // Otherwise we send the message to an internal queue for the target residing on our broker. It's then the
// broker's job to route the message to the target's P2P queue. // broker's job to route the message to the target's P2P queue.
val internalTargetQueue = (address as? ArtemisAddress)?.queueName ?: throw IllegalArgumentException("Not an Artemis address") val internalTargetQueue = (address as? ArtemisAddress)?.queueName
?: throw IllegalArgumentException("Not an Artemis address")
state.locked { state.locked {
createQueueIfAbsent(internalTargetQueue, producerSession!!) createQueueIfAbsent(internalTargetQueue, producerSession!!)
} }

View File

@ -25,13 +25,14 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.LifeCycle import net.corda.core.internal.LifeCycle
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.SerializationDefaults.RPC_SERVER_CONTEXT import net.corda.core.serialization.SerializationDefaults.RPC_SERVER_CONTEXT
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.utilities.* import net.corda.core.utilities.*
import net.corda.node.internal.security.AuthorizingSubject import net.corda.node.internal.security.AuthorizingSubject
import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.serialization.kryo.RpcServerObservableSerializer
import net.corda.node.services.logging.pushToLoggingContext import net.corda.node.services.logging.pushToLoggingContext
import net.corda.node.serialization.amqp.RpcServerObservableSerializer
import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.externalTrace import net.corda.nodeapi.externalTrace
import net.corda.nodeapi.impersonatedActor import net.corda.nodeapi.impersonatedActor
@ -55,6 +56,8 @@ import java.util.*
import java.util.concurrent.* import java.util.concurrent.*
import kotlin.concurrent.thread import kotlin.concurrent.thread
private typealias ObservableSubscriptionMap = Cache<InvocationId, ObservableSubscription>
data class RPCServerConfiguration( data class RPCServerConfiguration(
/** The number of threads to use for handling RPC requests */ /** The number of threads to use for handling RPC requests */
val rpcThreadPoolSize: Int, val rpcThreadPoolSize: Int,
@ -416,19 +419,22 @@ class RPCServer(
/* /*
* We construct an observable context on each RPC request. If subsequently a nested Observable is encountered this * We construct an observable context on each RPC request. If subsequently a nested Observable is encountered this
* same context is propagated by the instrumented KryoPool. This way all observations rooted in a single RPC will be * same context is propagated by serialization context. This way all observations rooted in a single RPC will be
* muxed correctly. Note that the context construction itself is quite cheap. * muxed correctly. Note that the context construction itself is quite cheap.
*/ */
inner class ObservableContext( inner class ObservableContext(
val observableMap: ObservableSubscriptionMap, override val observableMap: ObservableSubscriptionMap,
val clientAddressToObservables: ConcurrentHashMap<SimpleString, HashSet<InvocationId>>, override val clientAddressToObservables: ConcurrentHashMap<SimpleString, HashSet<InvocationId>>,
val deduplicationIdentity: String, override val deduplicationIdentity: String,
val clientAddress: SimpleString override val clientAddress: SimpleString
) { ) : ObservableContextInterface {
private val serializationContextWithObservableContext = RpcServerObservableSerializer.createContext(this) private val serializationContextWithObservableContext = RpcServerObservableSerializer.createContext(
observableContext = this,
serializationContext = SerializationDefaults.RPC_SERVER_CONTEXT)
fun sendMessage(serverToClient: RPCApi.ServerToClient) { override fun sendMessage(serverToClient: RPCApi.ServerToClient) {
sendJobQueue.put(RpcSendJob.Send(contextDatabaseOrNull, clientAddress, serializationContextWithObservableContext, serverToClient)) sendJobQueue.put(RpcSendJob.Send(contextDatabaseOrNull, clientAddress,
serializationContextWithObservableContext, serverToClient))
} }
} }
@ -488,4 +494,4 @@ class ObservableSubscription(
val subscription: Subscription val subscription: Subscription
) )
typealias ObservableSubscriptionMap = Cache<InvocationId, ObservableSubscription>

View File

@ -20,12 +20,12 @@ import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
import rx.Observable import rx.Observable
import rx.Scheduler import rx.Scheduler
import java.nio.file.Path import java.nio.file.Path

View File

@ -13,6 +13,7 @@ package net.corda.node.utilities.registration
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.utilities.contextLogger
import net.corda.node.NodeRegistrationOption import net.corda.node.NodeRegistrationOption
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration
@ -22,6 +23,7 @@ import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.util.io.pem.PemObject import org.bouncycastle.util.io.pem.PemObject
import java.io.StringWriter import java.io.StringWriter
@ -226,6 +228,10 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService:
CORDA_CLIENT_CA, CORDA_CLIENT_CA,
CertRole.NODE_CA) { CertRole.NODE_CA) {
companion object {
val logger = contextLogger()
}
override fun onSuccess(nodeCAKeyPair: KeyPair, certificates: List<X509Certificate>) { override fun onSuccess(nodeCAKeyPair: KeyPair, certificates: List<X509Certificate>) {
createSSLKeystore(nodeCAKeyPair, certificates) createSSLKeystore(nodeCAKeyPair, certificates)
createTruststore(certificates.last()) createTruststore(certificates.last())
@ -240,7 +246,10 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService:
certificates.first(), certificates.first(),
nodeCAKeyPair, nodeCAKeyPair,
config.myLegalName.x500Principal, config.myLegalName.x500Principal,
sslKeyPair.public) sslKeyPair.public,
crlDistPoint = config.tlsCertCrlDistPoint?.toString(),
crlIssuer = if (config.tlsCertCrlIssuer != null) X500Name(config.tlsCertCrlIssuer) else null)
logger.info("Generated TLS certificate: $sslCert")
setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates) setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates)
} }
println("SSL private key and certificate stored in ${config.sslKeystore}.") println("SSL private key and certificate stored in ${config.sslKeystore}.")

View File

@ -0,0 +1,101 @@
package net.corda.node.internal.serialization
import net.corda.client.rpc.internal.ObservableContext as ClientObservableContext
import net.corda.core.internal.ThreadBox
import net.corda.core.context.Trace
import net.corda.node.internal.serialization.testutils.AMQPRoundTripRPCSerializationScheme
import net.corda.node.internal.serialization.testutils.TestObservableContext as ServerObservableContext
import net.corda.node.services.messaging.ObservableSubscription
import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput
import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput
import co.paralleluniverse.common.util.SameThreadExecutor
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.RemovalListener
import com.nhaarman.mockito_kotlin.mock
import net.corda.client.rpc.internal.serialization.amqp.RpcClientObservableSerializer
import net.corda.node.internal.serialization.testutils.serializationContext
import net.corda.node.serialization.amqp.RpcServerObservableSerializer
import net.corda.nodeapi.RPCApi
import org.apache.activemq.artemis.api.core.SimpleString
import org.junit.Test
import rx.Notification
import rx.Observable
import rx.Subscription
import rx.subjects.UnicastSubject
import java.time.Instant
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
class RoundTripObservableSerializerTests {
private fun getID() = Trace.InvocationId("test1", Instant.now())
private fun subscriptionMap(
id: Trace.InvocationId
) : Cache<Trace.InvocationId, ObservableSubscription> {
val subMap: Cache<Trace.InvocationId, ObservableSubscription> = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
.build()
subMap.put(id, ObservableSubscription(mock<Subscription>()))
return subMap
}
private val observablesToReap = ThreadBox(object {
var observables = ArrayList<Trace.InvocationId>()
})
private fun createRpcObservableMap(): Cache<Trace.InvocationId, UnicastSubject<Notification<*>>> {
val onObservableRemove = RemovalListener<Trace.InvocationId, UnicastSubject<Notification<*>>> { key, value, cause ->
val observableId = key!!
observablesToReap.locked { observables.add(observableId) }
}
return Caffeine.newBuilder().weakValues().removalListener(onObservableRemove).executor(SameThreadExecutor.getExecutor()).build()
}
@Test
fun roundTripTest1() {
val serializationScheme = AMQPRoundTripRPCSerializationScheme(
serializationContext, emptySet(), ConcurrentHashMap())
// Fake up a message ID, needs to be used on both "sides". The server setting it in the subscriptionMap,
// the client as a property of the deserializer which, in the actual RPC client, is pulled off of
// the received message
val id : Trace.InvocationId = getID()
val serverObservableContext = ServerObservableContext(
subscriptionMap(id),
clientAddressToObservables = ConcurrentHashMap(),
deduplicationIdentity = "thisIsATest",
clientAddress = SimpleString("clientAddress"))
val serverSerializer = serializationScheme.rpcServerSerializerFactory(serverObservableContext)
val clientObservableContext = ClientObservableContext(
callSiteMap = null,
observableMap = createRpcObservableMap(),
hardReferenceStore = Collections.synchronizedSet(mutableSetOf<Observable<*>>())
)
val clientSerializer = serializationScheme.rpcClientSerializerFactory(clientObservableContext, id)
// What we're actually going to serialize then deserialize
val obs = Observable.create<Int>({ 12 })
val serverSerializationContext = RpcServerObservableSerializer.createContext(
serializationContext, serverObservableContext)
val clientSerializationContext = RpcClientObservableSerializer.createContext(
serializationContext, clientObservableContext).withProperty(RPCApi.RpcRequestOrObservableIdKey, id)
val blob = SerializationOutput(serverSerializer).serialize(obs, serverSerializationContext)
val obs2 = DeserializationInput(clientSerializer).deserialize(blob, clientSerializationContext)
}
}

View File

@ -0,0 +1,84 @@
package net.corda.node.internal.serialization
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import com.nhaarman.mockito_kotlin.mock
import net.corda.core.context.Trace
import net.corda.node.internal.serialization.testutils.*
import net.corda.node.serialization.amqp.RpcServerObservableSerializer
import net.corda.node.services.messaging.ObservableSubscription
import net.corda.nodeapi.internal.serialization.AllWhitelist
import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
import org.apache.activemq.artemis.api.core.SimpleString
import org.junit.Test
import rx.Observable
import rx.Subscription
import java.time.Instant
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class RpcServerObservableSerializerTests {
private fun subscriptionMap(): Cache<Trace.InvocationId, ObservableSubscription> {
val subMap: Cache<Trace.InvocationId, ObservableSubscription> = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
.build()
subMap.put(Trace.InvocationId("test1", Instant.now()), ObservableSubscription(mock<Subscription>()))
return subMap
}
@Test
fun canSerializerBeRegistered() {
val sf = SerializerFactory(AllWhitelist, javaClass.classLoader)
try {
sf.register(RpcServerObservableSerializer())
} catch (e: Exception) {
throw Error("Observable serializer must be registerable with factory, unexpected exception - ${e.message}")
}
}
@Test
fun canAssociateWithContext() {
val observable = TestObservableContext(
subscriptionMap(),
clientAddressToObservables = ConcurrentHashMap(),
deduplicationIdentity = "thisIsATest",
clientAddress = SimpleString("clientAddress"))
val newContext = RpcServerObservableSerializer.createContext(serializationContext, observable)
assertEquals(1, newContext.properties.size)
assertTrue(newContext.properties.containsKey(RpcServerObservableSerializer.RpcObservableContextKey))
assertEquals(observable, newContext.properties[RpcServerObservableSerializer.RpcObservableContextKey])
}
@Test
fun serialiseFakeObservable() {
val testClientAddress = "clientAddres"
val observable = TestObservableContext(
subscriptionMap(),
clientAddressToObservables = ConcurrentHashMap(),
deduplicationIdentity = "thisIsATest",
clientAddress = SimpleString(testClientAddress))
val sf = SerializerFactory(AllWhitelist, javaClass.classLoader).apply {
register(RpcServerObservableSerializer())
}
val obs = Observable.create<Int>({ 12 })
val newContext = RpcServerObservableSerializer.createContext(serializationContext, observable)
try {
SerializationOutput(sf).serializeAndReturnSchema(obs, newContext)
} catch (e: Exception) {
throw Error("Serialization of observable should not throw - ${e.message}")
}
}
}

View File

@ -0,0 +1,55 @@
package net.corda.node.internal.serialization.testutils
import net.corda.client.rpc.internal.serialization.amqp.RpcClientObservableSerializer
import net.corda.core.context.Trace
import net.corda.core.cordapp.Cordapp
import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationCustomSerializer
import net.corda.node.serialization.amqp.RpcServerObservableSerializer
import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.internal.serialization.AllWhitelist
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
import net.corda.nodeapi.internal.serialization.amqp.AbstractAMQPSerializationScheme
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
import java.util.concurrent.ConcurrentHashMap
import net.corda.client.rpc.internal.ObservableContext as ClientObservableContext
/**
* Special serialization context for the round trip tests that allows for both server and client RPC
* operations
*/
class AMQPRoundTripRPCSerializationScheme(
private val serializationContext: SerializationContext,
cordappCustomSerializers: Set<SerializationCustomSerializer<*, *>>,
serializerFactoriesForContexts: MutableMap<Pair<ClassWhitelist, ClassLoader>, SerializerFactory>)
: AbstractAMQPSerializationScheme(
cordappCustomSerializers, serializerFactoriesForContexts
) {
override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory {
return SerializerFactory(AllWhitelist, javaClass.classLoader).apply {
register(RpcClientObservableSerializer)
}
}
override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory {
return SerializerFactory(AllWhitelist, javaClass.classLoader).apply {
register(RpcServerObservableSerializer())
}
}
override fun canDeserializeVersion(
magic: CordaSerializationMagic,
target: SerializationContext.UseCase) = true
fun rpcClientSerializerFactory(observableContext: ClientObservableContext, id: Trace.InvocationId) =
rpcClientSerializerFactory(
RpcClientObservableSerializer.createContext(serializationContext, observableContext)
.withProperty(RPCApi.RpcRequestOrObservableIdKey, id))
fun rpcServerSerializerFactory(observableContext: TestObservableContext) =
rpcServerSerializerFactory(
RpcServerObservableSerializer.createContext(serializationContext, observableContext))
}

View File

@ -0,0 +1,18 @@
package net.corda.node.internal.serialization.testutils
import com.github.benmanes.caffeine.cache.Cache
import net.corda.core.context.Trace
import net.corda.node.services.messaging.ObservableContextInterface
import net.corda.node.services.messaging.ObservableSubscription
import net.corda.nodeapi.RPCApi
import org.apache.activemq.artemis.api.core.SimpleString
import java.util.concurrent.ConcurrentHashMap
class TestObservableContext(
override val observableMap: Cache<Trace.InvocationId, ObservableSubscription>,
override val clientAddressToObservables: ConcurrentHashMap<SimpleString, HashSet<Trace.InvocationId>>,
override val deduplicationIdentity: String,
override val clientAddress: SimpleString
) : ObservableContextInterface {
override fun sendMessage(serverToClient: RPCApi.ServerToClient) { }
}

View File

@ -0,0 +1,17 @@
package net.corda.node.internal.serialization.testutils
import net.corda.core.serialization.SerializationContext
import net.corda.nodeapi.internal.serialization.AllWhitelist
import net.corda.nodeapi.internal.serialization.SerializationContextImpl
import net.corda.nodeapi.internal.serialization.amqp.amqpMagic
val serializationProperties: MutableMap<Any, Any> = mutableMapOf()
val serializationContext = SerializationContextImpl(
preferredSerializationVersion = amqpMagic,
deserializationClassLoader = ClassLoader.getSystemClassLoader(),
whitelist = AllWhitelist,
properties = serializationProperties,
objectReferencesEnabled = false,
useCase = SerializationContext.UseCase.Testing,
encoding = null)

View File

@ -25,6 +25,7 @@ import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test import org.junit.Test
import java.net.URI import java.net.URI
import java.net.URL
import java.nio.file.Paths import java.nio.file.Paths
import java.util.* import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -42,6 +43,27 @@ class NodeConfigurationImplTest {
configDebugOptions(false, null) configDebugOptions(false, null)
} }
@Test
fun `can't have tlsCertCrlDistPoint null when tlsCertCrlIssuer is given`() {
val configValidationResult = configTlsCertCrlOptions(null, "C=US, L=New York, OU=Corda, O=R3 HoldCo LLC, CN=Corda Root CA").validate()
assertTrue { configValidationResult.isNotEmpty() }
assertThat(configValidationResult.first()).contains("tlsCertCrlDistPoint needs to be specified when tlsCertCrlIssuer is not NULL")
}
@Test
fun `tlsCertCrlIssuer validation fails when misconfigured`() {
val configValidationResult = configTlsCertCrlOptions(URL("http://test.com/crl"), "Corda Root CA").validate()
assertTrue { configValidationResult.isNotEmpty() }
assertThat(configValidationResult.first()).contains("Error when parsing tlsCertCrlIssuer:")
}
@Test
fun `can't have tlsCertCrlDistPoint null when crlCheckSoftFail is false`() {
val configValidationResult = configTlsCertCrlOptions(null, null, false).validate()
assertTrue { configValidationResult.isNotEmpty() }
assertThat(configValidationResult.first()).contains("tlsCertCrlDistPoint needs to be specified when crlCheckSoftFail is FALSE")
}
@Test @Test
fun `check devModeOptions flag helper`() { fun `check devModeOptions flag helper`() {
assertTrue { configDebugOptions(true, null).shouldCheckCheckpoints() } assertTrue { configDebugOptions(true, null).shouldCheckCheckpoints() }
@ -155,6 +177,10 @@ class NodeConfigurationImplTest {
return testConfiguration.copy(devMode = devMode, devModeOptions = devModeOptions) return testConfiguration.copy(devMode = devMode, devModeOptions = devModeOptions)
} }
private fun configTlsCertCrlOptions(tlsCertCrlDistPoint: URL?, tlsCertCrlIssuer: String?, crlCheckSoftFail: Boolean = true): NodeConfiguration {
return testConfiguration.copy(tlsCertCrlDistPoint = tlsCertCrlDistPoint, tlsCertCrlIssuer = tlsCertCrlIssuer, crlCheckSoftFail = crlCheckSoftFail)
}
private fun testConfiguration(dataSourceProperties: Properties): NodeConfigurationImpl { private fun testConfiguration(dataSourceProperties: Properties): NodeConfigurationImpl {
return testConfiguration.copy(dataSourceProperties = dataSourceProperties) return testConfiguration.copy(dataSourceProperties = dataSourceProperties)
} }
@ -190,7 +216,8 @@ class NodeConfigurationImplTest {
rpcSettings = rpcSettings, rpcSettings = rpcSettings,
relay = null, relay = null,
enterpriseConfiguration = EnterpriseConfiguration((MutualExclusionConfiguration(false, "", 20000, 40000))), enterpriseConfiguration = EnterpriseConfiguration((MutualExclusionConfiguration(false, "", 20000, 40000))),
crlCheckSoftFail = true crlCheckSoftFail = true,
tlsCertCrlDistPoint = null
) )
} }
} }

View File

@ -35,6 +35,7 @@ import net.corda.testing.node.MockServices.Companion.makeTestDataSourcePropertie
import net.corda.testing.node.internal.MOCK_VERSION_INFO import net.corda.testing.node.internal.MOCK_VERSION_INFO
import org.apache.activemq.artemis.api.core.Message.HDR_VALIDATED_USER import org.apache.activemq.artemis.api.core.Message.HDR_VALIDATED_USER
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.ActiveMQConnectionTimedOutException
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.After import org.junit.After
@ -50,6 +51,7 @@ import java.util.concurrent.TimeUnit.MILLISECONDS
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNull import kotlin.test.assertNull
import kotlin.test.assertTrue
class ArtemisMessagingTest { class ArtemisMessagingTest {
companion object { companion object {
@ -86,7 +88,8 @@ class ArtemisMessagingTest {
doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(null).whenever(it).jmxMonitoringHttpPort
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
doReturn(EnterpriseConfiguration(MutualExclusionConfiguration(false, "", 20000, 40000))).whenever(it).enterpriseConfiguration doReturn(EnterpriseConfiguration(MutualExclusionConfiguration(false, "", 20000, 40000))).whenever(it).enterpriseConfiguration
doReturn(P2PMessagingRetryConfiguration(5.seconds, 3, backoffBase=1.0)).whenever(it).p2pMessagingRetry doReturn(P2PMessagingRetryConfiguration(5.seconds, 3, backoffBase = 1.0)).whenever(it).p2pMessagingRetry
} }
LogHelper.setLevel(PersistentUniquenessProvider::class) LogHelper.setLevel(PersistentUniquenessProvider::class)
database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true), rigorousMock()) database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true), rigorousMock())
@ -148,6 +151,41 @@ class ArtemisMessagingTest {
assertNull(receivedMessages.poll(200, MILLISECONDS)) assertNull(receivedMessages.poll(200, MILLISECONDS))
} }
@Test
fun `client should fail if message exceed maxMessageSize limit`() {
val (messagingClient, receivedMessages) = createAndStartClientAndServer()
val message = messagingClient.createMessage(TOPIC, data = ByteArray(MAX_MESSAGE_SIZE))
messagingClient.send(message, messagingClient.myAddress)
val actual: Message = receivedMessages.take()
assertTrue(ByteArray(MAX_MESSAGE_SIZE).contentEquals(actual.data.bytes))
assertNull(receivedMessages.poll(200, MILLISECONDS))
val tooLagerMessage = messagingClient.createMessage(TOPIC, data = ByteArray(MAX_MESSAGE_SIZE + 1))
assertThatThrownBy {
messagingClient.send(tooLagerMessage, messagingClient.myAddress)
}.isInstanceOf(IllegalArgumentException::class.java)
.hasMessageContaining("Message exceeds maxMessageSize network parameter")
assertNull(receivedMessages.poll(200, MILLISECONDS))
}
@Test
fun `server should not process if incoming message exceed maxMessageSize limit`() {
val (messagingClient, receivedMessages) = createAndStartClientAndServer(clientMaxMessageSize = 100_000, serverMaxMessageSize = 50_000)
val message = messagingClient.createMessage(TOPIC, data = ByteArray(50_000))
messagingClient.send(message, messagingClient.myAddress)
val actual: Message = receivedMessages.take()
assertTrue(ByteArray(50_000).contentEquals(actual.data.bytes))
assertNull(receivedMessages.poll(200, MILLISECONDS))
val tooLagerMessage = messagingClient.createMessage(TOPIC, data = ByteArray(100_000))
assertThatThrownBy {
messagingClient.send(tooLagerMessage, messagingClient.myAddress)
}.isInstanceOf(ActiveMQConnectionTimedOutException::class.java)
assertNull(receivedMessages.poll(200, MILLISECONDS))
}
@Test @Test
fun `platform version is included in the message`() { fun `platform version is included in the message`() {
val (messagingClient, receivedMessages) = createAndStartClientAndServer(platformVersion = 3) val (messagingClient, receivedMessages) = createAndStartClientAndServer(platformVersion = 3)
@ -325,12 +363,15 @@ class ArtemisMessagingTest {
messagingClient!!.start() messagingClient!!.start()
} }
private fun createAndStartClientAndServer(platformVersion: Int = 1, dontAckCondition: (msg: ReceivedMessage) -> Boolean = { false }): Pair<P2PMessagingClient, BlockingQueue<ReceivedMessage>> { private fun createAndStartClientAndServer(platformVersion: Int = 1, serverMaxMessageSize: Int = MAX_MESSAGE_SIZE,
clientMaxMessageSize: Int = MAX_MESSAGE_SIZE,
dontAckCondition: (msg: ReceivedMessage) -> Boolean = { false }
): Pair<P2PMessagingClient, BlockingQueue<ReceivedMessage>> {
val receivedMessages = LinkedBlockingQueue<ReceivedMessage>() val receivedMessages = LinkedBlockingQueue<ReceivedMessage>()
createMessagingServer().start() createMessagingServer(maxMessageSize = serverMaxMessageSize).start()
val messagingClient = createMessagingClient(platformVersion = platformVersion) val messagingClient = createMessagingClient(platformVersion = platformVersion, maxMessageSize = clientMaxMessageSize)
messagingClient.addMessageHandler(TOPIC) { message, _, handle -> messagingClient.addMessageHandler(TOPIC) { message, _, handle ->
if (dontAckCondition(message)) return@addMessageHandler if (dontAckCondition(message)) return@addMessageHandler
database.transaction { handle.insideDatabaseTransaction() } database.transaction { handle.insideDatabaseTransaction() }

View File

@ -68,6 +68,8 @@ class NetworkRegistrationHelperTest {
doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword
doReturn(nodeLegalName).whenever(it).myLegalName doReturn(nodeLegalName).whenever(it).myLegalName
doReturn("").whenever(it).emailAddress doReturn("").whenever(it).emailAddress
doReturn(null).whenever(it).tlsCertCrlDistPoint
doReturn(null).whenever(it).tlsCertCrlIssuer
} }
} }

View File

@ -11,9 +11,9 @@
package net.corda.testing.node.internal package net.corda.testing.node.internal
import net.corda.client.mock.Generator import net.corda.client.mock.Generator
import net.corda.client.rpc.internal.serialization.kryo.KryoClientSerializationScheme
import net.corda.client.rpc.internal.RPCClient import net.corda.client.rpc.internal.RPCClient
import net.corda.client.rpc.internal.CordaRPCClientConfigurationImpl import net.corda.client.rpc.internal.CordaRPCClientConfigurationImpl
import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.context.AuthServiceId import net.corda.core.context.AuthServiceId
import net.corda.core.context.Trace import net.corda.core.context.Trace
@ -33,7 +33,7 @@ import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT import net.corda.nodeapi.internal.serialization.AMQP_RPC_CLIENT_CONTEXT
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.MAX_MESSAGE_SIZE import net.corda.testing.core.MAX_MESSAGE_SIZE
import net.corda.testing.driver.JmxPolicy import net.corda.testing.driver.JmxPolicy
@ -523,8 +523,8 @@ class RandomRpcUser {
val hostAndPort = NetworkHostAndPort.parse(args[1]) val hostAndPort = NetworkHostAndPort.parse(args[1])
val username = args[2] val username = args[2]
val password = args[3] val password = args[3]
KryoClientSerializationScheme.initialiseSerialization() AMQPClientSerializationScheme.initialiseSerialization()
val handle = RPCClient<RPCOps>(hostAndPort, null, serializationContext = KRYO_RPC_CLIENT_CONTEXT).start(rpcClass, username, password) val handle = RPCClient<RPCOps>(hostAndPort, null, serializationContext = AMQP_RPC_CLIENT_CONTEXT).start(rpcClass, username, password)
val callGenerators = rpcClass.declaredMethods.map { method -> val callGenerators = rpcClass.declaredMethods.map { method ->
Generator.sequence(method.parameters.map { Generator.sequence(method.parameters.map {
generatorStore[it.type] ?: throw Exception("No generator for ${it.type}") generatorStore[it.type] ?: throw Exception("No generator for ${it.type}")

View File

@ -12,7 +12,7 @@ package net.corda.smoketesting
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.CordaRPCConnection import net.corda.client.rpc.CordaRPCConnection
import net.corda.client.rpc.internal.serialization.kryo.KryoClientSerializationScheme import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
@ -74,7 +74,7 @@ class NodeProcess(
val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java") val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java")
val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(systemDefault()) val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(systemDefault())
val defaultNetworkParameters = run { val defaultNetworkParameters = run {
KryoClientSerializationScheme.createSerializationEnv().asContextEnv { AMQPClientSerializationScheme.createSerializationEnv().asContextEnv {
// There are no notaries in the network parameters for smoke test nodes. If this is required then we would // There are no notaries in the network parameters for smoke test nodes. If this is required then we would
// need to introduce the concept of a "network" which predefines the notaries, like the driver and MockNetwork // need to introduce the concept of a "network" which predefines the notaries, like the driver and MockNetwork
NetworkParametersCopier(testNetworkParameters()) NetworkParametersCopier(testNetworkParameters())

View File

@ -13,6 +13,7 @@ package net.corda.testing.core
import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.doAnswer import com.nhaarman.mockito_kotlin.doAnswer
import com.nhaarman.mockito_kotlin.whenever import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.DoNotImplement
import net.corda.core.internal.staticField import net.corda.core.internal.staticField
import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal.SerializationEnvironment
import net.corda.core.serialization.internal.effectiveSerializationEnv import net.corda.core.serialization.internal.effectiveSerializationEnv

View File

@ -12,13 +12,12 @@ package net.corda.testing.internal
import com.nhaarman.mockito_kotlin.doNothing import com.nhaarman.mockito_kotlin.doNothing
import com.nhaarman.mockito_kotlin.whenever import com.nhaarman.mockito_kotlin.whenever
import net.corda.client.rpc.internal.serialization.kryo.KryoClientSerializationScheme
import net.corda.core.DoNotImplement import net.corda.core.DoNotImplement
import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
import net.corda.core.serialization.internal.* import net.corda.core.serialization.internal.*
import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
import net.corda.node.serialization.kryo.KryoServerSerializationScheme import net.corda.node.serialization.kryo.KryoServerSerializationScheme
import net.corda.nodeapi.internal.serialization.* import net.corda.nodeapi.internal.serialization.*
import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.SerializationEnvironmentRule
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
@ -41,16 +40,16 @@ fun <T> withoutTestSerialization(callable: () -> T): T { // TODO: Delete this, s
internal fun createTestSerializationEnv(label: String): SerializationEnvironmentImpl { internal fun createTestSerializationEnv(label: String): SerializationEnvironmentImpl {
val factory = SerializationFactoryImpl().apply { val factory = SerializationFactoryImpl().apply {
registerScheme(KryoClientSerializationScheme())
registerScheme(KryoServerSerializationScheme())
registerScheme(AMQPClientSerializationScheme(emptyList())) registerScheme(AMQPClientSerializationScheme(emptyList()))
registerScheme(AMQPServerSerializationScheme(emptyList())) registerScheme(AMQPServerSerializationScheme(emptyList()))
// needed for checkpointing
registerScheme(KryoServerSerializationScheme())
} }
return object : SerializationEnvironmentImpl( return object : SerializationEnvironmentImpl(
factory, factory,
AMQP_P2P_CONTEXT, AMQP_P2P_CONTEXT,
KRYO_RPC_SERVER_CONTEXT, AMQP_RPC_SERVER_CONTEXT,
KRYO_RPC_CLIENT_CONTEXT, AMQP_RPC_CLIENT_CONTEXT,
AMQP_STORAGE_CONTEXT, AMQP_STORAGE_CONTEXT,
KRYO_CHECKPOINT_CONTEXT KRYO_CHECKPOINT_CONTEXT
) { ) {

View File

@ -32,6 +32,7 @@ apply plugin: 'kotlin'
apply plugin: 'application' apply plugin: 'application'
evaluationDependsOn(':tools:explorer:capsule') evaluationDependsOn(':tools:explorer:capsule')
evaluationDependsOn(':webserver:webcapsule')
mainClassName = 'net.corda.demobench.DemoBench' mainClassName = 'net.corda.demobench.DemoBench'
applicationDefaultJvmArgs = [ applicationDefaultJvmArgs = [

View File

@ -11,13 +11,12 @@
package net.corda.demobench package net.corda.demobench
import javafx.scene.image.Image import javafx.scene.image.Image
import net.corda.client.rpc.internal.serialization.kryo.KryoClientSerializationScheme import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal.SerializationEnvironmentImpl
import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.demobench.views.DemoBenchView import net.corda.demobench.views.DemoBenchView
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme
import tornadofx.* import tornadofx.*
import java.io.InputStreamReader import java.io.InputStreamReader
import java.nio.charset.StandardCharsets.UTF_8 import java.nio.charset.StandardCharsets.UTF_8
@ -69,8 +68,7 @@ class DemoBench : App(DemoBenchView::class) {
private fun initialiseSerialization() { private fun initialiseSerialization() {
nodeSerializationEnv = SerializationEnvironmentImpl( nodeSerializationEnv = SerializationEnvironmentImpl(
SerializationFactoryImpl().apply { SerializationFactoryImpl().apply {
registerScheme(KryoClientSerializationScheme()) registerScheme(AMQPClientSerializationScheme(emptyList()))
registerScheme(AMQPClientSerializationScheme())
}, },
AMQP_P2P_CONTEXT) AMQP_P2P_CONTEXT)
} }

View File

@ -37,6 +37,7 @@ import org.bouncycastle.util.io.Streams
import org.junit.ClassRule import org.junit.ClassRule
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
import java.lang.Thread.sleep
import java.net.ConnectException import java.net.ConnectException
import kotlin.test.assertTrue import kotlin.test.assertTrue
import kotlin.test.fail import kotlin.test.fail
@ -75,7 +76,7 @@ class SSHServerTest : IntegrationTest() {
// The driver will automatically pick up the annotated flows below // The driver will automatically pick up the annotated flows below
driver { driver {
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user),
customOverrides = mapOf("sshd" to mapOf("port" to 2222))) customOverrides = mapOf("sshd" to mapOf("port" to 2222)) /*, startInSameProcess = true */)
node.getOrThrow() node.getOrThrow()
val session = JSch().getSession("u", "localhost", 2222) val session = JSch().getSession("u", "localhost", 2222)