Moved file uploading to RPC interface.

This commit is contained in:
Clinton Alexander
2017-01-16 17:57:41 +00:00
committed by Clinton Alexander
parent d4b6e32682
commit 537ffae113
22 changed files with 165 additions and 191 deletions

View File

@ -107,6 +107,11 @@ interface CordaRPCOps : RPCOps {
*/ */
fun uploadAttachment(jar: InputStream): SecureHash fun uploadAttachment(jar: InputStream): SecureHash
/**
* Uploads a file by data type to the node and returns a meaningful string to the user
*/
fun uploadFile(dataType: String, name: String?, file: InputStream): String
/** /**
* Returns the node-local current time. * Returns the node-local current time.
*/ */

View File

@ -10,6 +10,8 @@ import net.corda.core.toFuture
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import rx.Observable import rx.Observable
import java.io.File
import java.io.InputStream
import java.security.KeyPair import java.security.KeyPair
import java.security.PrivateKey import java.security.PrivateKey
import java.security.PublicKey import java.security.PublicKey
@ -216,6 +218,16 @@ interface KeyManagementService {
fun freshKey(): KeyPair fun freshKey(): KeyPair
} }
// TODO: Move and document
interface FileUploader {
/**
* Accepts the data in the given input stream, and returns some sort of useful return message that will be sent
* back to the user in the response.
*/
fun upload(file: InputStream): String
fun accepts(prefix: String): Boolean
}
/** /**
* A sketch of an interface to a simple key/value storage system. Intended for persistence of simple blobs like * A sketch of an interface to a simple key/value storage system. Intended for persistence of simple blobs like
* transactions, serialised flow state machines and so on. Again, this isn't intended to imply lack of SQL or * transactions, serialised flow state machines and so on. Again, this isn't intended to imply lack of SQL or
@ -232,6 +244,9 @@ interface StorageService {
/** Provides access to storage of arbitrary JAR files (which may contain only data, no code). */ /** Provides access to storage of arbitrary JAR files (which may contain only data, no code). */
val attachments: AttachmentStorage val attachments: AttachmentStorage
/** Provides file uploads of arbitrary files to services **/
val uploaders: List<FileUploader>
val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage
} }

View File

@ -21,6 +21,7 @@ class ArgsParser {
.withRequiredArg() .withRequiredArg()
.defaultsTo("node.conf") .defaultsTo("node.conf")
private val logToConsoleArg = optionParser.accepts("log-to-console", "If set, prints logging to the console as well as to a file.") private val logToConsoleArg = optionParser.accepts("log-to-console", "If set, prints logging to the console as well as to a file.")
private val isWebserverArg = optionParser.accepts("webserver")
private val helpArg = optionParser.accepts("help").forHelp() private val helpArg = optionParser.accepts("help").forHelp()
fun parse(vararg args: String): CmdLineOptions { fun parse(vararg args: String): CmdLineOptions {
@ -30,13 +31,14 @@ class ArgsParser {
} }
val baseDirectory = Paths.get(optionSet.valueOf(baseDirectoryArg)).normalize().toAbsolutePath() val baseDirectory = Paths.get(optionSet.valueOf(baseDirectoryArg)).normalize().toAbsolutePath()
val configFile = baseDirectory / optionSet.valueOf(configFileArg) val configFile = baseDirectory / optionSet.valueOf(configFileArg)
return CmdLineOptions(baseDirectory, configFile, optionSet.has(helpArg), optionSet.has(logToConsoleArg)) val isWebserver = optionSet.has(isWebserverArg)
return CmdLineOptions(baseDirectory, configFile, optionSet.has(helpArg), optionSet.has(logToConsoleArg), isWebserver)
} }
fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink) fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink)
} }
data class CmdLineOptions(val baseDirectory: Path, val configFile: Path?, val help: Boolean, val logToConsole: Boolean) { data class CmdLineOptions(val baseDirectory: Path, val configFile: Path?, val help: Boolean, val logToConsole: Boolean, val isWebserver: Boolean) {
fun loadConfig(allowMissingConfig: Boolean = false, configOverrides: Map<String, Any?> = emptyMap()): Config { fun loadConfig(allowMissingConfig: Boolean = false, configOverrides: Map<String, Any?> = emptyMap()): Config {
return ConfigHelper.loadConfig(baseDirectory, configFile, allowMissingConfig, configOverrides) return ConfigHelper.loadConfig(baseDirectory, configFile, allowMissingConfig, configOverrides)
} }

View File

@ -7,6 +7,7 @@ import net.corda.core.utilities.Emoji
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.utilities.ANSIProgressObserver import net.corda.node.utilities.ANSIProgressObserver
import net.corda.node.webserver.WebServer
import org.fusesource.jansi.Ansi import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiConsole import org.fusesource.jansi.AnsiConsole
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -78,33 +79,30 @@ fun main(args: Array<String>) {
log.info("VM ${info.vmName} ${info.vmVendor} ${info.vmVersion}") log.info("VM ${info.vmName} ${info.vmVendor} ${info.vmVersion}")
log.info("Machine: ${InetAddress.getLocalHost().hostName}") log.info("Machine: ${InetAddress.getLocalHost().hostName}")
log.info("Working Directory: ${cmdlineOptions.baseDirectory}") log.info("Working Directory: ${cmdlineOptions.baseDirectory}")
log.info("Started as webserver: ${cmdlineOptions.isWebserver}")
try { try {
cmdlineOptions.baseDirectory.createDirectories() cmdlineOptions.baseDirectory.createDirectories()
val node = conf.createNode() if(!cmdlineOptions.isWebserver) {
node.start() val node = conf.createNode()
printPluginsAndServices(node) node.start()
printPluginsAndServices(node)
thread { node.networkMapRegistrationFuture.success {
Thread.sleep(30.seconds.toMillis()) val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0
while (!node.networkMapRegistrationFuture.isDone) { printBasicNodeInfo("Node started up and registered in $elapsed sec")
printBasicNodeInfo("Waiting for response from network map ...")
Thread.sleep(30.seconds.toMillis()) if (renderBasicInfoToConsole)
ANSIProgressObserver(node.smm)
} failure {
log.error("Error during network map registration", it)
exitProcess(1)
} }
node.run()
} else {
WebServer(conf).start()
} }
node.networkMapRegistrationFuture.success {
val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0
printBasicNodeInfo("Node started up and registered in $elapsed sec")
if (renderBasicInfoToConsole)
ANSIProgressObserver(node.smm)
} failure {
log.error("Error during network map registration", it)
exitProcess(1)
}
node.run()
} catch (e: Exception) { } catch (e: Exception) {
log.error("Exception during node startup", e) log.error("Exception during node startup", e)
exitProcess(1) exitProcess(1)

View File

@ -2,8 +2,6 @@
package net.corda.node.driver package net.corda.node.driver
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
@ -15,7 +13,6 @@ import net.corda.core.crypto.Party
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.ServiceType import net.corda.core.node.services.ServiceType
import net.corda.core.utilities.ApiUtils
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.services.User import net.corda.node.services.User
import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.ConfigHelper
@ -26,9 +23,9 @@ import net.corda.node.services.messaging.CordaRPCClient
import net.corda.node.services.messaging.NodeMessagingClient import net.corda.node.services.messaging.NodeMessagingClient
import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.services.transactions.RaftValidatingNotaryService
import net.corda.node.utilities.JsonSupport
import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.node.utilities.ServiceIdentityGenerator
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request
import org.slf4j.Logger import org.slf4j.Logger
import java.io.File import java.io.File
import java.net.* import java.net.*
@ -44,9 +41,6 @@ import java.util.concurrent.*
import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.TimeUnit.MILLISECONDS
import java.util.concurrent.TimeUnit.SECONDS import java.util.concurrent.TimeUnit.SECONDS
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import com.sun.corba.se.spi.presentation.rmi.StubAdapter.request
import org.bouncycastle.crypto.tls.ConnectionEnd.client
import okhttp3.Request
/** /**
@ -201,6 +195,7 @@ fun <DI : DriverDSLExposedInterface, D : DriverDSLInternalInterface, A> genericD
return returnValue return returnValue
} catch (exception: Throwable) { } catch (exception: Throwable) {
println("Driver shutting down because of exception $exception") println("Driver shutting down because of exception $exception")
exception.printStackTrace()
throw exception throw exception
} finally { } finally {
driverDsl.shutdown() driverDsl.shutdown()
@ -423,19 +418,23 @@ open class DriverDSL(
} }
private fun queryWebserver(configuration: FullNodeConfiguration): HostAndPort? { private fun queryWebserver(configuration: FullNodeConfiguration): HostAndPort? {
val protocol = if(configuration.useHTTPS) { "https://" } else { "http://" } val protocol = if (configuration.useHTTPS) {
"https://"
} else {
"http://"
}
val url = URL(protocol + configuration.webAddress.toString() + "/api/status") val url = URL(protocol + configuration.webAddress.toString() + "/api/status")
val client = OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).readTimeout(60, TimeUnit.SECONDS).build() val client = OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).readTimeout(60, TimeUnit.SECONDS).build()
val retries = 5 val retries = 5
for(i in 0..retries) { for (i in 0..retries) {
try { try {
val response = client.newCall(Request.Builder().url(url).build()).execute() val response = client.newCall(Request.Builder().url(url).build()).execute()
if (response.isSuccessful && (response.body().string() == "started")) { if (response.isSuccessful && (response.body().string() == "started")) {
return configuration.webAddress return configuration.webAddress
} }
} catch(e: ConnectException) { } catch(e: ConnectException) {
log.debug("Retrying webserver info at ${ configuration.webAddress}") log.debug("Retrying webserver info at ${configuration.webAddress}")
} }
} }
@ -467,7 +466,6 @@ open class DriverDSL(
configOverrides = mapOf( configOverrides = mapOf(
"myLegalName" to networkMapLegalName, "myLegalName" to networkMapLegalName,
"artemisAddress" to networkMapAddress.toString(), "artemisAddress" to networkMapAddress.toString(),
"webAddress" to apiAddress.toString(),
"extraAdvertisedServiceIds" to "", "extraAdvertisedServiceIds" to "",
"useTestClock" to useTestClock "useTestClock" to useTestClock
) )
@ -533,7 +531,7 @@ open class DriverDSL(
executorService: ScheduledExecutorService, executorService: ScheduledExecutorService,
nodeConf: FullNodeConfiguration, nodeConf: FullNodeConfiguration,
debugPort: Int?): ListenableFuture<Process> { debugPort: Int?): ListenableFuture<Process> {
val className = "net.corda.node.webserver.MainKt" // cannot directly get class for this, so just use string val className = "net.corda.node.Corda" // cannot directly get class for this, so just use string
val separator = System.getProperty("file.separator") val separator = System.getProperty("file.separator")
val classpath = System.getProperty("java.class.path") val classpath = System.getProperty("java.class.path")
val path = System.getProperty("java.home") + separator + "bin" + separator + "java" val path = System.getProperty("java.home") + separator + "bin" + separator + "java"
@ -548,7 +546,7 @@ open class DriverDSL(
listOf( listOf(
"-cp", classpath, className, "-cp", classpath, className,
"--base-directory", nodeConf.baseDirectory.toString(), "--base-directory", nodeConf.baseDirectory.toString(),
"--web-address", nodeConf.webAddress.toString()) "--webserver")
val builder = ProcessBuilder(javaArgs) val builder = ProcessBuilder(javaArgs)
builder.redirectError(Paths.get("error.$className.log").toFile()) builder.redirectError(Paths.get("error.$className.log").toFile())
builder.inheritIO() builder.inheritIO()

View File

@ -51,6 +51,7 @@ import net.corda.node.utilities.databaseTransaction
import org.apache.activemq.artemis.utils.ReusableLatch import org.apache.activemq.artemis.utils.ReusableLatch
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.slf4j.Logger import org.slf4j.Logger
import java.io.File
import java.nio.file.FileAlreadyExistsException import java.nio.file.FileAlreadyExistsException
import java.nio.file.Path import java.nio.file.Path
import java.security.KeyPair import java.security.KeyPair
@ -103,11 +104,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
// low-performance prototyping period. // low-performance prototyping period.
protected abstract val serverThread: AffinityExecutor protected abstract val serverThread: AffinityExecutor
// Objects in this list will be scanned by the DataUploadServlet and can be handed new data via HTTP.
// Don't mutate this after startup.
protected val _servicesThatAcceptUploads = ArrayList<AcceptsFileUpload>()
val servicesThatAcceptUploads: List<AcceptsFileUpload> = _servicesThatAcceptUploads
private val flowFactories = ConcurrentHashMap<Class<*>, (Party) -> FlowLogic<*>>() private val flowFactories = ConcurrentHashMap<Class<*>, (Party) -> FlowLogic<*>>()
protected val partyKeys = mutableSetOf<KeyPair>() protected val partyKeys = mutableSetOf<KeyPair>()
@ -225,6 +221,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
customServices.clear() customServices.clear()
customServices.addAll(buildPluginServices(tokenizableServices)) customServices.addAll(buildPluginServices(tokenizableServices))
val uploaders: List<FileUploader> = listOf(storageServices.first.attachments as NodeAttachmentService) +
customServices.filter { it is AcceptsFileUpload }.map { it as AcceptsFileUpload }
(storage as StorageServiceImpl).initUploaders(uploaders)
// TODO: uniquenessProvider creation should be inside makeNotaryService(), but notary service initialisation // TODO: uniquenessProvider creation should be inside makeNotaryService(), but notary service initialisation
// depends on smm, while smm depends on tokenizableServices, which uniquenessProvider is part of // depends on smm, while smm depends on tokenizableServices, which uniquenessProvider is part of
advertisedServices.singleOrNull { it.type.isNotary() }?.let { advertisedServices.singleOrNull { it.type.isNotary() }?.let {
@ -350,9 +350,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
val service = serviceConstructor.apply(services) val service = serviceConstructor.apply(services)
serviceList.add(service) serviceList.add(service)
tokenizableServices.add(service) tokenizableServices.add(service)
if (service is AcceptsFileUpload) {
_servicesThatAcceptUploads += service
}
} }
return serviceList return serviceList
} }
@ -482,7 +479,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
val attachments = makeAttachmentStorage(dir) val attachments = makeAttachmentStorage(dir)
val checkpointStorage = DBCheckpointStorage() val checkpointStorage = DBCheckpointStorage()
val transactionStorage = DBTransactionStorage() val transactionStorage = DBTransactionStorage()
_servicesThatAcceptUploads += attachments
val stateMachineTransactionMappingStorage = DBTransactionMappingStorage() val stateMachineTransactionMappingStorage = DBTransactionMappingStorage()
return Pair( return Pair(
constructStorageService(attachments, transactionStorage, stateMachineTransactionMappingStorage), constructStorageService(attachments, transactionStorage, stateMachineTransactionMappingStorage),

View File

@ -108,6 +108,10 @@ class CordaRPCOpsImpl(
override fun attachmentExists(id: SecureHash) = services.storageService.attachments.openAttachment(id) != null override fun attachmentExists(id: SecureHash) = services.storageService.attachments.openAttachment(id) != null
override fun uploadAttachment(jar: InputStream) = services.storageService.attachments.importAttachment(jar) override fun uploadAttachment(jar: InputStream) = services.storageService.attachments.importAttachment(jar)
override fun currentNodeTime(): Instant = Instant.now(services.clock) override fun currentNodeTime(): Instant = Instant.now(services.clock)
override fun uploadFile(dataType: String, name: String?, file: InputStream): String {
val acceptor = services.storageService.uploaders.firstOrNull { it.accepts(dataType) }
return acceptor?.upload(file) ?: throw RuntimeException("Cannot find file upload acceptor for $dataType")
}
override fun partyFromKey(key: CompositeKey) = services.identityService.partyFromKey(key) override fun partyFromKey(key: CompositeKey) = services.identityService.partyFromKey(key)
override fun partyFromName(name: String) = services.identityService.partyFromName(name) override fun partyFromName(name: String) = services.identityService.partyFromName(name)

View File

@ -1,5 +1,7 @@
package net.corda.node.services.api package net.corda.node.services.api
import net.corda.core.crypto.SecureHash
import net.corda.core.node.services.FileUploader
import java.io.InputStream import java.io.InputStream
/** /**
@ -7,16 +9,12 @@ import java.io.InputStream
* *
* TODO: In future, also accept uploads over the MQ interface too. * TODO: In future, also accept uploads over the MQ interface too.
*/ */
interface AcceptsFileUpload { interface AcceptsFileUpload: FileUploader {
/** A string that prefixes the URLs, e.g. "attachments" or "interest-rates". Should be OK for URLs. */ /** A string that prefixes the URLs, e.g. "attachments" or "interest-rates". Should be OK for URLs. */
val dataTypePrefix: String val dataTypePrefix: String
/** What file extensions are acceptable for the file to be handed to upload() */ /** What file extensions are acceptable for the file to be handed to upload() */
val acceptableFileExtensions: List<String> val acceptableFileExtensions: List<String>
/** override fun accepts(prefix: String) = prefix == dataTypePrefix
* Accepts the data in the given input stream, and returns some sort of useful return message that will be sent
* back to the user in the response.
*/
fun upload(data: InputStream): String
} }

View File

@ -36,6 +36,7 @@ import net.corda.node.services.messaging.ArtemisMessagingComponent.NetworkMapAdd
import net.i2p.crypto.eddsa.EdDSAPrivateKey import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSAPublicKey import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.commons.fileupload.MultipartStream
import org.objenesis.strategy.StdInstantiatorStrategy import org.objenesis.strategy.StdInstantiatorStrategy
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -146,6 +147,7 @@ private class RPCKryo(observableSerializer: Serializer<Observable<Any>>? = null)
register(BufferedInputStream::class.java, InputStreamSerializer) register(BufferedInputStream::class.java, InputStreamSerializer)
register(Class.forName("sun.net.www.protocol.jar.JarURLConnection\$JarURLInputStream"), InputStreamSerializer) register(Class.forName("sun.net.www.protocol.jar.JarURLConnection\$JarURLInputStream"), InputStreamSerializer)
register(MultipartStream.ItemInputStream::class.java, InputStreamSerializer)
noReferencesWithin<WireTransaction>() noReferencesWithin<WireTransaction>()
@ -208,6 +210,7 @@ private class RPCKryo(observableSerializer: Serializer<Observable<Any>>? = null)
register(SimpleString::class.java) register(SimpleString::class.java)
register(ServiceEntry::class.java) register(ServiceEntry::class.java)
// Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway. // Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway.
register(RuntimeException::class.java)
register(IllegalArgumentException::class.java) register(IllegalArgumentException::class.java)
register(ArrayIndexOutOfBoundsException::class.java) register(ArrayIndexOutOfBoundsException::class.java)
register(IndexOutOfBoundsException::class.java) register(IndexOutOfBoundsException::class.java)

View File

@ -1,12 +1,15 @@
package net.corda.node.services.persistence package net.corda.node.services.persistence
import net.corda.core.node.services.AttachmentStorage import net.corda.core.node.services.*
import net.corda.core.node.services.StateMachineRecordedTransactionMappingStorage
import net.corda.core.node.services.TransactionStorage
import net.corda.core.node.services.TxWritableStorageService
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
open class StorageServiceImpl(override val attachments: AttachmentStorage, open class StorageServiceImpl(override val attachments: AttachmentStorage,
override val validatedTransactions: TransactionStorage, override val validatedTransactions: TransactionStorage,
override val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage) override val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage)
: SingletonSerializeAsToken(), TxWritableStorageService : SingletonSerializeAsToken(), TxWritableStorageService {
lateinit override var uploaders: List<FileUploader>
fun initUploaders(uploadersList: List<FileUploader>) {
uploaders = uploadersList
}
}

View File

@ -1,44 +0,0 @@
package net.corda.node.webserver
import joptsimple.OptionParser
import net.corda.node.driver.driver
import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.FullNodeConfiguration
import java.io.File
import java.nio.file.Paths
import kotlin.system.exitProcess
fun main(args: Array<String>) {
// TODO: Print basic webserver info
val parser = OptionParser()
val baseDirectoryArg = parser.accepts("base-directory", "The directory to put all files under").withRequiredArg()
val webAddressArg = parser.accepts("web-address", "The web address for this server to bind").withOptionalArg()
val logToConsoleArg = parser.accepts("log-to-console", "If set, prints logging to the console as well as to a file.")
val helpArg = parser.accepts("help").forHelp()
val cmdlineOptions = try {
parser.parse(*args)
} catch (ex: Exception) {
println("Unknown command line arguments: ${ex.message}")
exitProcess(1)
}
// Maybe render command line help.
if (cmdlineOptions.has(helpArg)) {
parser.printHelpOn(System.out)
exitProcess(0)
}
// Set up logging.
if (cmdlineOptions.has(logToConsoleArg)) {
// This property is referenced from the XML config file.
System.setProperty("consoleLogLevel", "info")
}
val baseDirectoryPath = Paths.get(cmdlineOptions.valueOf(baseDirectoryArg))
val config = ConfigHelper.loadConfig(baseDirectoryPath)
println("Starting server")
val nodeConf = FullNodeConfiguration(baseDirectoryPath, config)
val server = WebServer(nodeConf).start()
println("Exiting")
}

View File

@ -1,15 +0,0 @@
package net.corda.node.webserver
import net.corda.node.driver.driver
import net.corda.node.services.config.FullNodeConfiguration
fun main(args: Array<String>) {
// TODO: Print basic webserver info
System.setProperty("consoleLogLevel", "info")
driver {
val node = startNode().get()
//WebServer(node.configuration).start() // Old in memory way
startWebserver(node)
waitForAllNodesToFinish()
}
}

View File

@ -102,6 +102,7 @@ class WebServer(val config: FullNodeConfiguration) {
private fun buildServletContextHandler(localRpc: CordaRPCOps): ServletContextHandler { private fun buildServletContextHandler(localRpc: CordaRPCOps): ServletContextHandler {
return ServletContextHandler().apply { return ServletContextHandler().apply {
contextPath = "/" contextPath = "/"
setAttribute("rpc", localRpc)
addServlet(DataUploadServlet::class.java, "/upload/*") addServlet(DataUploadServlet::class.java, "/upload/*")
addServlet(AttachmentDownloadServlet::class.java, "/attachments/*") addServlet(AttachmentDownloadServlet::class.java, "/attachments/*")
@ -111,8 +112,6 @@ class WebServer(val config: FullNodeConfiguration) {
resourceConfig.register(APIServerImpl(localRpc)) resourceConfig.register(APIServerImpl(localRpc))
val webAPIsOnClasspath = pluginRegistries.flatMap { x -> x.webApis } val webAPIsOnClasspath = pluginRegistries.flatMap { x -> x.webApis }
println("NUM PLUGINS: ${pluginRegistries.size}")
println("NUM WEBAPIS: ${webAPIsOnClasspath.size}")
for (webapi in webAPIsOnClasspath) { for (webapi in webAPIsOnClasspath) {
log.info("Add plugin web API from attachment $webapi") log.info("Add plugin web API from attachment $webapi")
val customAPI = try { val customAPI = try {

View File

@ -1,5 +1,6 @@
package net.corda.node.webserver.servlets package net.corda.node.webserver.servlets
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.services.api.AcceptsFileUpload import net.corda.node.services.api.AcceptsFileUpload
@ -12,26 +13,19 @@ import javax.servlet.http.HttpServletResponse
/** /**
* Accepts binary streams, finds the right [AcceptsFileUpload] implementor and hands the stream off to it. * Accepts binary streams, finds the right [AcceptsFileUpload] implementor and hands the stream off to it.
*/ */
class DataUploadServlet : HttpServlet() { class DataUploadServlet: HttpServlet() {
private val log = loggerFor<DataUploadServlet>() private val log = loggerFor<DataUploadServlet>()
override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) { override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) {
val node = servletContext.getAttribute("node") as Node
@Suppress("DEPRECATION") // Bogus warning due to superclass static method being deprecated. @Suppress("DEPRECATION") // Bogus warning due to superclass static method being deprecated.
val isMultipart = ServletFileUpload.isMultipartContent(req) val isMultipart = ServletFileUpload.isMultipartContent(req)
val rpc = servletContext.getAttribute("rpc") as CordaRPCOps
if (!isMultipart) { if (!isMultipart) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "This end point is for data uploads only.") resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "This end point is for data uploads only.")
return return
} }
val acceptor: AcceptsFileUpload? = findAcceptor(node, req)
if (acceptor == null) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Got a file upload request for an unknown data type")
return
}
val upload = ServletFileUpload() val upload = ServletFileUpload()
val iterator = upload.getItemIterator(req) val iterator = upload.getItemIterator(req)
val messages = ArrayList<String>() val messages = ArrayList<String>()
@ -43,18 +37,15 @@ class DataUploadServlet : HttpServlet() {
while (iterator.hasNext()) { while (iterator.hasNext()) {
val item = iterator.next() val item = iterator.next()
if (item.name != null && !acceptor.acceptableFileExtensions.any { item.name.endsWith(it) }) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
"${item.name}: Must be have a filename ending in one of: ${acceptor.acceptableFileExtensions}")
return
}
log.info("Receiving ${item.name}") log.info("Receiving ${item.name}")
item.openStream().use { try {
val message = acceptor.upload(it) val dataType = req.pathInfo.substring(1).substringBefore('/')
log.info("${item.name} successfully accepted: $message") messages += rpc.uploadFile(dataType, item.name, item.openStream())
messages += message log.info("${item.name} successfully accepted: ${messages.last()}")
} catch(e: RuntimeException) {
println(e)
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Got a file upload request for an unknown data type")
} }
} }
@ -62,8 +53,4 @@ class DataUploadServlet : HttpServlet() {
val writer = resp.writer val writer = resp.writer
messages.forEach { writer.println(it) } messages.forEach { writer.println(it) }
} }
private fun findAcceptor(node: Node, req: HttpServletRequest): AcceptsFileUpload? {
return node.servicesThatAcceptUploads.firstOrNull { req.pathInfo.substring(1).substringBefore('/') == it.dataTypePrefix }
}
} }

View File

@ -17,7 +17,8 @@ class ArgsParserTest {
baseDirectory = workingDirectory, baseDirectory = workingDirectory,
configFile = workingDirectory / "node.conf", configFile = workingDirectory / "node.conf",
help = false, help = false,
logToConsole = false)) logToConsole = false,
isWebserver = false))
} }
@Test @Test
@ -28,7 +29,8 @@ class ArgsParserTest {
baseDirectory = expectedBaseDir, baseDirectory = expectedBaseDir,
configFile = expectedBaseDir / "node.conf", configFile = expectedBaseDir / "node.conf",
help = false, help = false,
logToConsole = false)) logToConsole = false,
isWebserver = false))
} }
@Test @Test
@ -39,7 +41,8 @@ class ArgsParserTest {
baseDirectory = baseDirectory, baseDirectory = baseDirectory,
configFile = baseDirectory / "node.conf", configFile = baseDirectory / "node.conf",
help = false, help = false,
logToConsole = false)) logToConsole = false,
isWebserver = false))
} }
@Test @Test
@ -49,7 +52,8 @@ class ArgsParserTest {
baseDirectory = workingDirectory, baseDirectory = workingDirectory,
configFile = workingDirectory / "different.conf", configFile = workingDirectory / "different.conf",
help = false, help = false,
logToConsole = false)) logToConsole = false,
isWebserver = false))
} }
@Test @Test
@ -60,7 +64,19 @@ class ArgsParserTest {
baseDirectory = workingDirectory, baseDirectory = workingDirectory,
configFile = configFile, configFile = configFile,
help = false, help = false,
logToConsole = false)) logToConsole = false,
isWebserver = false))
}
@Test
fun `just webserver `() {
val cmdLineOptions = parser.parse("--webserver")
assertThat(cmdLineOptions).isEqualTo(CmdLineOptions(
baseDirectory = workingDirectory,
configFile = workingDirectory / "node.conf",
help = false,
logToConsole = false,
isWebserver = true))
} }
@Test @Test

View File

@ -50,8 +50,9 @@ private class BankOfCordaDriver {
driver(dsl = { driver(dsl = {
val user = User("user1", "test", permissions = setOf(startFlowPermission<CashFlow>(), startFlowPermission<IssuerFlow.IssuanceRequester>())) val user = User("user1", "test", permissions = setOf(startFlowPermission<CashFlow>(), startFlowPermission<IssuerFlow.IssuanceRequester>()))
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type))) startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
startNode("BankOfCorda", rpcUsers = listOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.USD")))) val bankOfCorda = startNode("BankOfCorda", rpcUsers = listOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.USD"))))
startNode("BigCorporation", rpcUsers = listOf(user)) startNode("BigCorporation", rpcUsers = listOf(user))
startWebserver(bankOfCorda.get())
waitForAllNodesToFinish() waitForAllNodesToFinish()
}, isDebug = true) }, isDebug = true)
} }
@ -75,7 +76,8 @@ private class BankOfCordaDriver {
} }
} }
catch (e: Exception) { catch (e: Exception) {
printHelp(parser) println("Exception occurred: $e \n ${e.printStackTrace()}")
exitProcess(1)
} }
} }
} }

View File

@ -1,5 +1,7 @@
package net.corda.irs package net.corda.irs
import com.google.common.util.concurrent.Futures
import net.corda.core.getOrThrow
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.irs.api.NodeInterestRates import net.corda.irs.api.NodeInterestRates
import net.corda.node.driver.driver import net.corda.node.driver.driver
@ -11,9 +13,16 @@ import net.corda.node.services.transactions.SimpleNotaryService
*/ */
fun main(args: Array<String>) { fun main(args: Array<String>) {
driver(dsl = { driver(dsl = {
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.type))).get() val (controller, nodeA, nodeB) = Futures.allAsList(
startNode("Bank A") startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.type))),
startNode("Bank B") startNode("Bank A"),
startNode("Bank B")
).getOrThrow()
startWebserver(controller)
startWebserver(nodeA)
startWebserver(nodeB)
waitForAllNodesToFinish() waitForAllNodesToFinish()
}, useTestClock = true, isDebug = true) }, useTestClock = true, isDebug = true)
} }

View File

@ -23,6 +23,7 @@ import net.corda.node.utilities.JDBCHashedTable
import net.corda.node.utilities.localDate import net.corda.node.utilities.localDate
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.statements.InsertStatement import org.jetbrains.exposed.sql.statements.InsertStatement
import org.jetbrains.exposed.sql.transactions.transaction
import java.io.InputStream import java.io.InputStream
import java.math.BigDecimal import java.math.BigDecimal
import java.security.KeyPair import java.security.KeyPair
@ -110,7 +111,10 @@ object NodeInterestRates {
override fun upload(data: InputStream): String { override fun upload(data: InputStream): String {
val fixes = parseFile(data.bufferedReader().readText()) val fixes = parseFile(data.bufferedReader().readText())
oracle.knownFixes = fixes // TODO: Look into why knownFixes requires a transaction
transaction {
oracle.knownFixes = fixes
}
val msg = "Interest rates oracle accepted ${fixes.size} new interest rate fixes" val msg = "Interest rates oracle accepted ${fixes.size} new interest rate fixes"
println(msg) println(msg)
return msg return msg

View File

@ -24,7 +24,6 @@ fun main(args: Array<String>) {
/** Interface for using the notary demo API from a client. */ /** Interface for using the notary demo API from a client. */
private class NotaryDemoClientApi(val rpc: CordaRPCOps) { private class NotaryDemoClientApi(val rpc: CordaRPCOps) {
private val notary by lazy { private val notary by lazy {
rpc.networkMapUpdates().first.first { it.advertisedServices.any { it.info.type.isNotary() } }.notaryIdentity rpc.networkMapUpdates().first.first { it.advertisedServices.any { it.info.type.isNotary() } }.notaryIdentity
} }
@ -37,17 +36,21 @@ private class NotaryDemoClientApi(val rpc: CordaRPCOps) {
private val TRANSACTION_COUNT = 10 private val TRANSACTION_COUNT = 10
} }
/** Makes a call to the demo api to start transaction notarisation. */ /** Makes calls to the node rpc to start transaction notarisation. */
fun startNotarisation() { fun startNotarisation() {
val response = notarise(TRANSACTION_COUNT) notarise(TRANSACTION_COUNT)
println(response)
} }
fun notarise(count: Int): String { fun notarise(count: Int) {
val transactions = buildTransactions(count) val transactions = buildTransactions(count)
val signers = notariseTransactions(transactions) val signers = notariseTransactions(transactions)
val transactionSigners = transactions.zip(signers).map {
val (tx, signer) = it
"Tx [${tx.tx.id.prefixChars()}..] signed by $signer"
}.joinToString("\n")
return buildResponse(transactions, signers) println("Notary: \"${notary.name}\", with composite key: ${notary.owningKey}\n" +
"Notarised ${transactions.size} transactions:\n" + transactionSigners)
} }
/** /**
@ -72,20 +75,7 @@ private class NotaryDemoClientApi(val rpc: CordaRPCOps) {
val signatureFutures = transactions.map { val signatureFutures = transactions.map {
rpc.startFlow(NotaryFlow::Client, it).returnValue.toBlocking().toFuture() rpc.startFlow(NotaryFlow::Client, it).returnValue.toBlocking().toFuture()
} }
val signers = signatureFutures.map { it.get().by.toStringShort() } return signatureFutures.map { it.get().by.toStringShort() }
return signers
}
/** Builds a response for the caller containing the list of transaction ids and corresponding signer keys. */
private fun buildResponse(transactions: List<SignedTransaction>, signers: List<String>): String {
val transactionSigners = transactions.zip(signers).map {
val (tx, signer) = it
"Tx [${tx.tx.id.prefixChars()}..] signed by $signer"
}.joinToString("\n")
val response = "Notary: \"${notary.name}\", with composite key: ${notary.owningKey}\n" +
"Notarised ${transactions.size} transactions:\n" + transactionSigners
return response
} }
} }

View File

@ -1,9 +1,9 @@
package net.corda.vega package net.corda.vega
import com.google.common.util.concurrent.Futures
import com.opengamma.strata.product.common.BuySell import com.opengamma.strata.product.common.BuySell
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.node.driver.NodeHandle
import net.corda.node.driver.driver import net.corda.node.driver.driver
import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.testing.IntegrationTestCategory import net.corda.testing.IntegrationTestCategory
@ -12,14 +12,11 @@ import net.corda.vega.api.PortfolioApi
import net.corda.vega.api.PortfolioApiUtils import net.corda.vega.api.PortfolioApiUtils
import net.corda.vega.api.SwapDataModel import net.corda.vega.api.SwapDataModel
import net.corda.vega.api.SwapDataView import net.corda.vega.api.SwapDataView
import net.corda.vega.portfolio.Portfolio
import org.junit.Test import org.junit.Test
import java.math.BigDecimal import java.math.BigDecimal
import java.time.LocalDate import java.time.LocalDate
import java.util.*
import java.util.concurrent.Future
class SimmValuationTest: IntegrationTestCategory { class SimmValuationTest : IntegrationTestCategory {
private companion object { private companion object {
// SIMM demo can only currently handle one valuation date due to a lack of market data or a market data source. // SIMM demo can only currently handle one valuation date due to a lack of market data or a market data source.
val valuationDate = LocalDate.parse("2016-06-06") val valuationDate = LocalDate.parse("2016-06-06")
@ -31,23 +28,20 @@ class SimmValuationTest: IntegrationTestCategory {
@Test fun `runs SIMM valuation demo`() { @Test fun `runs SIMM valuation demo`() {
driver(isDebug = true) { driver(isDebug = true) {
startNode("Controller", setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow() startNode("Controller", setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow()
val nodeA = getSimmNodeApi(startNode(nodeALegalName)) val (nodeA, nodeB) = Futures.allAsList(startNode(nodeALegalName), startNode(nodeBLegalName)).getOrThrow()
val nodeB = getSimmNodeApi(startNode(nodeBLegalName)) val (nodeAApi, nodeBApi) = Futures.allAsList(startWebserver(nodeA), startWebserver(nodeB))
val nodeBParty = getPartyWithName(nodeA, nodeBLegalName) .getOrThrow()
val nodeAParty = getPartyWithName(nodeB, nodeALegalName) .map { HttpApi.fromHostAndPort(it, "api/simmvaluationdemo") }
val nodeBParty = getPartyWithName(nodeAApi, nodeBLegalName)
val nodeAParty = getPartyWithName(nodeBApi, nodeALegalName)
assert(createTradeBetween(nodeA, nodeBParty, testTradeId)) assert(createTradeBetween(nodeAApi, nodeBParty, testTradeId))
assert(tradeExists(nodeB, nodeAParty, testTradeId)) assert(tradeExists(nodeBApi, nodeAParty, testTradeId))
assert(runValuationsBetween(nodeA, nodeBParty)) assert(runValuationsBetween(nodeAApi, nodeBParty))
assert(valuationExists(nodeB, nodeAParty)) assert(valuationExists(nodeBApi, nodeAParty))
} }
} }
private fun getSimmNodeApi(futureNode: Future<NodeHandle>): HttpApi {
val nodeAddr = futureNode.getOrThrow().configuration.webAddress
return HttpApi.fromHostAndPort(nodeAddr, "api/simmvaluationdemo")
}
private fun getPartyWithName(partyApi: HttpApi, countryparty: String): PortfolioApi.ApiParty = private fun getPartyWithName(partyApi: HttpApi, countryparty: String): PortfolioApi.ApiParty =
getAvailablePartiesFor(partyApi).counterparties.single { it.text == countryparty } getAvailablePartiesFor(partyApi).counterparties.single { it.text == countryparty }

View File

@ -1,5 +1,7 @@
package net.corda.vega package net.corda.vega
import com.google.common.util.concurrent.Futures
import net.corda.core.getOrThrow
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.node.driver.driver import net.corda.node.driver.driver
import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.SimpleNotaryService
@ -12,9 +14,16 @@ import net.corda.node.services.transactions.SimpleNotaryService
fun main(args: Array<String>) { fun main(args: Array<String>) {
driver(dsl = { driver(dsl = {
startNode("Controller", setOf(ServiceInfo(SimpleNotaryService.type))) startNode("Controller", setOf(ServiceInfo(SimpleNotaryService.type)))
startNode("Bank A") val (nodeA, nodeB, nodeC) = Futures.allAsList(
startNode("Bank B") startNode("Bank A"),
startNode("Bank C") startNode("Bank B"),
startNode("Bank C")
).getOrThrow()
startWebserver(nodeA)
startWebserver(nodeB)
startWebserver(nodeC)
waitForAllNodesToFinish() waitForAllNodesToFinish()
}, isDebug = true) }, isDebug = true)
} }

View File

@ -155,6 +155,7 @@ open class MockTransactionStorage : TransactionStorage {
@ThreadSafe @ThreadSafe
class MockStorageService(override val attachments: AttachmentStorage = MockAttachmentStorage(), class MockStorageService(override val attachments: AttachmentStorage = MockAttachmentStorage(),
override val validatedTransactions: TransactionStorage = MockTransactionStorage(), override val validatedTransactions: TransactionStorage = MockTransactionStorage(),
override val uploaders: List<FileUploader> = listOf<FileUploader>(),
override val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage()) override val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage())
: SingletonSerializeAsToken(), TxWritableStorageService : SingletonSerializeAsToken(), TxWritableStorageService