Refactor CordaRPCClient into new :client:rpc Gradle module. (#405)

* CORDA-305: Refactor CordaRPCClient into :client:rpc module

* CORDA-305: Remove the Kotlin test framework from the artifacts.

* CORDA-305: Migrate serialisation whitelist into node-api module.

* CORDA-305: Clean up unused RPC observables.

* CORDA-305: Add :client:rpc module to documentation tasks.

* CORDA-305: Include :finance into :client:rpc for its serialisable classes.

* CORDA-305: Move test classes into the correct directory.

* CORDA-305: Migrate :finance dependency from :client:rpc into DemoBench.

* CORDA-305: Update wording of TODO about handling Observables.
This commit is contained in:
Chris Rankin
2017-03-22 15:52:54 +00:00
committed by GitHub
parent 9a5bba9c04
commit 98266da41c
76 changed files with 248 additions and 165 deletions

View File

@ -0,0 +1,118 @@
package net.corda.nodeapi
import com.google.common.annotations.VisibleForTesting
import com.google.common.net.HostAndPort
import net.corda.nodeapi.config.SSLConfiguration
import net.corda.core.crypto.CompositeKey
import net.corda.core.messaging.MessageRecipientGroup
import net.corda.core.messaging.MessageRecipients
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.read
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SingletonSerializeAsToken
import java.security.KeyStore
/**
* The base class for Artemis services that defines shared data structures and SSL transport configuration.
*/
abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() {
companion object {
init {
System.setProperty("org.jboss.logging.provider", "slf4j")
}
// System users must contain an invalid RPC username character to prevent any chance of name clash which in this
// case is a forward slash
const val NODE_USER = "SystemUsers/Node"
const val PEER_USER = "SystemUsers/Peer"
const val INTERNAL_PREFIX = "internal."
const val PEERS_PREFIX = "${INTERNAL_PREFIX}peers."
const val SERVICES_PREFIX = "${INTERNAL_PREFIX}services."
const val CLIENTS_PREFIX = "clients."
const val P2P_QUEUE = "p2p.inbound"
const val RPC_REQUESTS_QUEUE = "rpc.requests"
const val RPC_QUEUE_REMOVALS_QUEUE = "rpc.qremovals"
const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications"
const val NETWORK_MAP_QUEUE = "${INTERNAL_PREFIX}networkmap"
/**
* Assuming the passed in target address is actually an ArtemisAddress will extract the host and port of the node. This should
* only be used in unit tests and the internals of the messaging services to keep addressing opaque for the future.
* N.B. Marked as JvmStatic to allow use in the inherited classes.
*/
@JvmStatic
@VisibleForTesting
fun toHostAndPort(target: MessageRecipients): HostAndPort {
val addr = target as? ArtemisMessagingComponent.ArtemisPeerAddress ?: throw IllegalArgumentException("Not an Artemis address")
return addr.hostAndPort
}
}
interface ArtemisAddress : MessageRecipients {
val queueName: String
}
interface ArtemisPeerAddress : ArtemisAddress, SingleMessageRecipient {
val hostAndPort: HostAndPort
}
@CordaSerializable
data class NetworkMapAddress(override val hostAndPort: HostAndPort) : SingleMessageRecipient, ArtemisPeerAddress {
override val queueName: String get() = NETWORK_MAP_QUEUE
}
/**
* This is the class used to implement [SingleMessageRecipient], for now. Note that in future this class
* may change or evolve and code that relies upon it being a simple host/port may not function correctly.
* For instance it may contain onion routing data.
*
* [NodeAddress] identifies a specific peer node and an associated queue. The queue may be the peer's own queue or
* an advertised service's queue.
*
* @param queueName The name of the queue this address is associated with.
* @param hostAndPort The address of the node.
*/
@CordaSerializable
data class NodeAddress(override val queueName: String, override val hostAndPort: HostAndPort) : ArtemisPeerAddress {
companion object {
fun asPeer(peerIdentity: CompositeKey, hostAndPort: HostAndPort): NodeAddress {
return NodeAddress("$PEERS_PREFIX${peerIdentity.toBase58String()}", hostAndPort)
}
fun asService(serviceIdentity: CompositeKey, hostAndPort: HostAndPort): NodeAddress {
return NodeAddress("$SERVICES_PREFIX${serviceIdentity.toBase58String()}", hostAndPort)
}
}
}
/**
* [ServiceAddress] implements [MessageRecipientGroup]. It holds a queue associated with a service advertised by
* zero or more nodes. Each advertising node has an associated consumer.
*
* By sending to such an address Artemis will pick a consumer (uses Round Robin by default) and sends the message
* there. We use this to establish sessions involving service counterparties.
*
* @param identity The service identity's owning key.
*/
data class ServiceAddress(val identity: CompositeKey) : ArtemisAddress, MessageRecipientGroup {
override val queueName: String = "$SERVICES_PREFIX${identity.toBase58String()}"
}
/** The config object is used to pass in the passwords for the certificate KeyStore and TrustStore */
abstract val config: SSLConfiguration?
/**
* Returns nothing if the keystore was opened OK or throws if not. Useful to check the password, as
* unfortunately Artemis tends to bury the exception when the password is wrong.
*/
fun checkStorePasswords() {
val config = config ?: return
config.keyStoreFile.read {
KeyStore.getInstance("JKS").load(it, config.keyStorePassword.toCharArray())
}
config.trustStoreFile.read {
KeyStore.getInstance("JKS").load(it, config.trustStorePassword.toCharArray())
}
}
}

View File

@ -0,0 +1,136 @@
@file:JvmName("RPCStructures")
package net.corda.nodeapi
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Registration
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.flows.FlowException
import net.corda.core.serialization.*
import net.corda.core.toFuture
import net.corda.core.toObservable
import org.apache.commons.fileupload.MultipartStream
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import rx.Notification
import rx.Observable
/** Global RPC logger */
val rpcLog: Logger by lazy { LoggerFactory.getLogger("net.corda.rpc") }
/** Used in the RPC wire protocol to wrap an observation with the handle of the observable it's intended for. */
data class MarshalledObservation(val forHandle: Int, val what: Notification<*>)
data class User(val username: String, val password: String, val permissions: Set<String>) {
override fun toString(): String = "${javaClass.simpleName}($username, permissions=$permissions)"
}
/** Records the protocol version in which this RPC was added. */
@Target(AnnotationTarget.FUNCTION)
@MustBeDocumented
annotation class RPCSinceVersion(val version: Int)
/** The contents of an RPC request message, separated from the MQ layer. */
data class ClientRPCRequestMessage(
val args: SerializedBytes<Array<Any>>,
val replyToAddress: String,
val observationsToAddress: String?,
val methodName: String,
val user: User
) {
companion object {
const val REPLY_TO = "reply-to"
const val OBSERVATIONS_TO = "observations-to"
const val METHOD_NAME = "method-name"
}
}
/**
* This is available to RPC implementations to query the validated [User] that is calling it. Each user has a set of
* permissions they're entitled to which can be used to control access.
*/
@JvmField
val CURRENT_RPC_USER: ThreadLocal<User> = ThreadLocal()
/**
* Thrown to indicate a fatal error in the RPC system itself, as opposed to an error generated by the invoked
* method.
*/
@CordaSerializable
open class RPCException(msg: String, cause: Throwable?) : RuntimeException(msg, cause) {
constructor(msg: String) : this(msg, null)
class DeadlineExceeded(rpcName: String) : RPCException("Deadline exceeded on call to $rpcName")
}
object ClassSerializer : Serializer<Class<*>>() {
override fun read(kryo: Kryo, input: Input, type: Class<Class<*>>): Class<*> {
val className = input.readString()
return Class.forName(className)
}
override fun write(kryo: Kryo, output: Output, clazz: Class<*>) {
output.writeString(clazz.name)
}
}
@CordaSerializable
class PermissionException(msg: String) : RuntimeException(msg)
object RPCKryoClientKey
object RPCKryoDispatcherKey
object RPCKryoQNameKey
object RPCKryoMethodNameKey
object RPCKryoLocationKey
// 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<Any>>) : CordaKryo(makeStandardClassResolver()) {
init {
DefaultKryoCustomizer.customize(this)
// RPC specific classes
register(Class::class.java, ClassSerializer)
register(MultipartStream.ItemInputStream::class.java, InputStreamSerializer)
register(MarshalledObservation::class.java, ImmutableClassSerializer(MarshalledObservation::class))
register(Observable::class.java, observableSerializer)
@Suppress("UNCHECKED_CAST")
register(ListenableFuture::class,
read = { kryo, input -> observableSerializer.read(kryo, input, Observable::class.java as Class<Observable<Any>>).toFuture() },
write = { kryo, output, obj -> observableSerializer.write(kryo, output, obj.toObservable()) }
)
register(
FlowException::class,
read = { kryo, input ->
val message = input.readString()
val cause = kryo.readObjectOrNull(input, Throwable::class.java)
FlowException(message, cause)
},
write = { kryo, output, obj ->
// The subclass may have overridden toString so we use that
val message = if (obj.javaClass != FlowException::class.java) obj.toString() else obj.message
output.writeString(message)
kryo.writeObjectOrNull(output, obj.cause, Throwable::class.java)
}
)
}
override fun getRegistration(type: Class<*>): Registration {
val annotated = context[RPCKryoQNameKey] != null
if (Observable::class.java.isAssignableFrom(type)) {
return if (annotated) super.getRegistration(Observable::class.java)
else throw IllegalStateException("This RPC was not annotated with @RPCReturnsObservables")
}
if (ListenableFuture::class.java.isAssignableFrom(type)) {
return if (annotated) super.getRegistration(ListenableFuture::class.java)
else throw IllegalStateException("This RPC was not annotated with @RPCReturnsObservables")
}
if (FlowException::class.java.isAssignableFrom(type))
return super.getRegistration(FlowException::class.java)
return super.getRegistration(type)
}
}

View File

@ -0,0 +1,57 @@
package net.corda.nodeapi.serialization
import com.esotericsoftware.kryo.KryoException
import com.google.common.net.HostAndPort
import net.corda.core.node.CordaPluginRegistry
import net.corda.core.serialization.SerializationCustomization
import org.apache.activemq.artemis.api.core.SimpleString
import rx.Notification
import rx.exceptions.OnErrorNotImplementedException
import java.math.BigDecimal
import java.time.LocalDate
import java.time.Period
import java.util.*
/**
* NOTE: We do not whitelist [HashMap] or [HashSet] since they are unstable under serialization.
*/
class DefaultWhitelist : CordaPluginRegistry() {
override fun customizeSerialization(custom: SerializationCustomization): Boolean {
custom.apply {
addToWhitelist(Array<Any>(0, {}).javaClass)
addToWhitelist(Notification::class.java)
addToWhitelist(Notification.Kind::class.java)
addToWhitelist(ArrayList::class.java)
addToWhitelist(listOf<Any>().javaClass) // EmptyList
addToWhitelist(Pair::class.java)
addToWhitelist(ByteArray::class.java)
addToWhitelist(UUID::class.java)
addToWhitelist(LinkedHashSet::class.java)
addToWhitelist(setOf<Unit>().javaClass) // EmptySet
addToWhitelist(Currency::class.java)
addToWhitelist(listOf(Unit).javaClass) // SingletonList
addToWhitelist(setOf(Unit).javaClass) // SingletonSet
addToWhitelist(mapOf(Unit to Unit).javaClass) // SingletonSet
addToWhitelist(HostAndPort::class.java)
addToWhitelist(SimpleString::class.java)
addToWhitelist(KryoException::class.java)
addToWhitelist(StringBuffer::class.java)
addToWhitelist(Unit::class.java)
addToWhitelist(java.io.ByteArrayInputStream::class.java)
addToWhitelist(java.lang.Class::class.java)
addToWhitelist(java.math.BigDecimal::class.java)
addToWhitelist(java.security.KeyPair::class.java)
addToWhitelist(java.time.Duration::class.java)
addToWhitelist(java.time.Instant::class.java)
addToWhitelist(java.time.LocalDate::class.java)
addToWhitelist(java.util.Collections.singletonMap("A", "B").javaClass)
addToWhitelist(java.util.LinkedHashMap::class.java)
addToWhitelist(BigDecimal::class.java)
addToWhitelist(LocalDate::class.java)
addToWhitelist(Period::class.java)
addToWhitelist(BitSet::class.java)
addToWhitelist(OnErrorNotImplementedException::class.java)
}
return true
}
}

View File

@ -0,0 +1,2 @@
# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry
net.corda.nodeapi.serialization.DefaultWhitelist