mirror of
https://github.com/corda/corda.git
synced 2025-06-19 15:43:52 +00:00
Added webserver project.
This commit is contained in:
committed by
Clinton Alexander
parent
3676425781
commit
fa257738e1
@ -111,7 +111,7 @@ class Node(override val configuration: FullNodeConfiguration,
|
|||||||
// serialisation/deserialisation work.
|
// serialisation/deserialisation work.
|
||||||
override val serverThread = AffinityExecutor.ServiceAffinityExecutor("Node thread", 1)
|
override val serverThread = AffinityExecutor.ServiceAffinityExecutor("Node thread", 1)
|
||||||
|
|
||||||
lateinit var webServer: Server
|
//lateinit var webServer: Server
|
||||||
var messageBroker: ArtemisMessagingServer? = null
|
var messageBroker: ArtemisMessagingServer? = null
|
||||||
|
|
||||||
// Avoid the lock being garbage collected. We don't really need to release it as the OS will do so for us
|
// Avoid the lock being garbage collected. We don't really need to release it as the OS will do so for us
|
||||||
@ -314,18 +314,17 @@ class Node(override val configuration: FullNodeConfiguration,
|
|||||||
override fun start(): Node {
|
override fun start(): Node {
|
||||||
alreadyRunningNodeCheck()
|
alreadyRunningNodeCheck()
|
||||||
super.start()
|
super.start()
|
||||||
|
// Only start the service API requests once the network map registration is complete
|
||||||
// Only start the service API requests once the network map registration is successfully complete
|
|
||||||
networkMapRegistrationFuture.success {
|
|
||||||
// This needs to be in a seperate thread so that we can reply to our own request to become RPC clients
|
|
||||||
thread(name = "WebServer") {
|
thread(name = "WebServer") {
|
||||||
try {
|
networkMapRegistrationFuture.getOrThrow()
|
||||||
webServer = initWebServer(connectLocalRpcAsNodeUser())
|
// TODO: Remove when cleanup
|
||||||
} catch(ex: Exception) {
|
//try {
|
||||||
// TODO: We need to decide if this is a fatal error, given the API is unavailable, or whether the API
|
// webServer = initWebServer(connectLocalRpcAsNodeUser())
|
||||||
// is not critical and we continue anyway.
|
//} catch(ex: Exception) {
|
||||||
log.error("Web server startup failed", ex)
|
// // TODO: We need to decide if this is a fatal error, given the API is unavailable, or whether the API
|
||||||
}
|
// // is not critical and we continue anyway.
|
||||||
|
// log.error("Web server startup failed", ex)
|
||||||
|
//}
|
||||||
// Begin exporting our own metrics via JMX.
|
// Begin exporting our own metrics via JMX.
|
||||||
JmxReporter.
|
JmxReporter.
|
||||||
forRegistry(services.monitoringService.metrics).
|
forRegistry(services.monitoringService.metrics).
|
||||||
@ -342,7 +341,6 @@ class Node(override val configuration: FullNodeConfiguration,
|
|||||||
build().
|
build().
|
||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
shutdownThread = thread(start = false) {
|
shutdownThread = thread(start = false) {
|
||||||
stop()
|
stop()
|
||||||
|
53
node/webserver/build.gradle
Normal file
53
node/webserver/build.gradle
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'java'
|
||||||
|
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||||
|
apply plugin: 'net.corda.plugins.publish-utils'
|
||||||
|
|
||||||
|
description 'Corda Webserver'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
||||||
|
}
|
||||||
|
jcenter()
|
||||||
|
maven {
|
||||||
|
url 'https://dl.bintray.com/kotlin/exposed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile project(':core')
|
||||||
|
compile project(':node')
|
||||||
|
|
||||||
|
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
// Web stuff: for HTTP[S] servlets
|
||||||
|
compile "org.eclipse.jetty:jetty-servlet:${jetty_version}"
|
||||||
|
compile "org.eclipse.jetty:jetty-webapp:${jetty_version}"
|
||||||
|
compile "javax.servlet:javax.servlet-api:3.1.0"
|
||||||
|
compile "org.jolokia:jolokia-agent-war:2.0.0-M1"
|
||||||
|
compile "commons-fileupload:commons-fileupload:1.3.2"
|
||||||
|
|
||||||
|
// Jersey for JAX-RS implementation for use in Jetty
|
||||||
|
compile "org.glassfish.jersey.core:jersey-server:${jersey_version}"
|
||||||
|
compile "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}"
|
||||||
|
compile "org.glassfish.jersey.containers:jersey-container-jetty-http:${jersey_version}"
|
||||||
|
// NOTE there is a Jackson version clash between jersey-media-json-jackson (v2.5.4) and jackson-module-kotlin (v.2.5.5)
|
||||||
|
// Have not found an Issue in the issue tracker for Jersey for this issue
|
||||||
|
compile ("org.glassfish.jersey.media:jersey-media-json-jackson:${jersey_version}") {
|
||||||
|
exclude group: 'com.fasterxml.jackson.core', module: 'jackson-annotations'
|
||||||
|
exclude group: 'com.fasterxml.jackson.core', module: 'jackson-databind'
|
||||||
|
exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core'
|
||||||
|
}
|
||||||
|
compile ("com.fasterxml.jackson.module:jackson-module-kotlin:${jackson_version}") {
|
||||||
|
exclude group: 'com.fasterxml.jackson.core', module: 'jackson-annotations'
|
||||||
|
}
|
||||||
|
compile "com.fasterxml.jackson.core:jackson-annotations:${jackson_version}"
|
||||||
|
}
|
||||||
|
|
||||||
|
task run(type: JavaExec) {
|
||||||
|
classpath = sourceSets.main.runtimeClasspath
|
||||||
|
main = 'net.corda.node.webserver.MainKt'
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package net.corda.node.webserver
|
||||||
|
|
||||||
|
import com.google.common.net.HostAndPort
|
||||||
|
import net.corda.node.driver.driver
|
||||||
|
import net.corda.node.services.User
|
||||||
|
import net.corda.node.services.config.ConfigHelper
|
||||||
|
import net.corda.node.services.config.FullNodeConfiguration
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
driver {
|
||||||
|
val node = startNode().get()
|
||||||
|
WebServer(node.nodeInfo, node.config).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateNodeConfiguration(): FullNodeConfiguration {
|
||||||
|
val messagingAddress = 10002
|
||||||
|
val apiAddress = HostAndPort.fromString("localhost:10003")
|
||||||
|
val name = "webserver-test"
|
||||||
|
val rpcUsers = listOf<User>()
|
||||||
|
|
||||||
|
val baseDirectory = Paths.get("build/webserver")
|
||||||
|
val configOverrides = mapOf(
|
||||||
|
"myLegalName" to name,
|
||||||
|
"basedir" to baseDirectory.normalize().toString(),
|
||||||
|
"artemisAddress" to messagingAddress.toString(),
|
||||||
|
"webAddress" to apiAddress.toString(),
|
||||||
|
"extraAdvertisedServiceIds" to listOf<String>(),
|
||||||
|
"networkMapAddress" to "",
|
||||||
|
"useTestClock" to false,
|
||||||
|
"rpcUsers" to rpcUsers.map {
|
||||||
|
mapOf(
|
||||||
|
"user" to it.username,
|
||||||
|
"password" to it.password,
|
||||||
|
"permissions" to it.permissions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val config = ConfigHelper.loadConfig(
|
||||||
|
baseDirectoryPath = baseDirectory,
|
||||||
|
allowMissingConfig = true,
|
||||||
|
configOverrides = configOverrides
|
||||||
|
)
|
||||||
|
|
||||||
|
return FullNodeConfiguration(config)
|
||||||
|
}
|
@ -0,0 +1,176 @@
|
|||||||
|
package net.corda.node.webserver
|
||||||
|
|
||||||
|
import com.google.common.net.HostAndPort
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
|
import net.corda.core.node.CordaPluginRegistry
|
||||||
|
import net.corda.core.node.NodeInfo
|
||||||
|
import net.corda.core.utilities.loggerFor
|
||||||
|
import net.corda.node.internal.Node
|
||||||
|
import net.corda.node.services.config.FullNodeConfiguration
|
||||||
|
import net.corda.node.services.config.NodeSSLConfiguration
|
||||||
|
import net.corda.node.services.messaging.ArtemisMessagingComponent
|
||||||
|
import net.corda.node.services.messaging.CordaRPCClient
|
||||||
|
import net.corda.node.servlets.AttachmentDownloadServlet
|
||||||
|
import net.corda.node.servlets.DataUploadServlet
|
||||||
|
import net.corda.node.servlets.ResponseFilter
|
||||||
|
import org.eclipse.jetty.server.*
|
||||||
|
import org.eclipse.jetty.server.handler.HandlerCollection
|
||||||
|
import org.eclipse.jetty.servlet.DefaultServlet
|
||||||
|
import org.eclipse.jetty.servlet.FilterHolder
|
||||||
|
import org.eclipse.jetty.servlet.ServletContextHandler
|
||||||
|
import org.eclipse.jetty.servlet.ServletHolder
|
||||||
|
import org.eclipse.jetty.util.ssl.SslContextFactory
|
||||||
|
import org.eclipse.jetty.webapp.WebAppContext
|
||||||
|
import org.glassfish.jersey.server.ResourceConfig
|
||||||
|
import org.glassfish.jersey.server.ServerProperties
|
||||||
|
import org.glassfish.jersey.servlet.ServletContainer
|
||||||
|
import java.lang.reflect.InvocationTargetException
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.*
|
||||||
|
import javax.servlet.DispatcherType
|
||||||
|
|
||||||
|
class WebServer(val nodeInfo: NodeInfo, val configuration: Config) {
|
||||||
|
private companion object {
|
||||||
|
val log = loggerFor<WebServer>()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val address = HostAndPort.fromString(configuration.getString("webAddress"))
|
||||||
|
private val sslConfig = object : NodeSSLConfiguration {
|
||||||
|
override val keyStorePassword: String
|
||||||
|
get() = throw UnsupportedOperationException()
|
||||||
|
override val trustStorePassword: String
|
||||||
|
get() = throw UnsupportedOperationException()
|
||||||
|
override val certificatesPath: Path
|
||||||
|
get() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
initWebServer(connectLocalRpcAsNodeUser())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initWebServer(localRpc: CordaRPCOps): Server {
|
||||||
|
// Note that the web server handlers will all run concurrently, and not on the node thread.
|
||||||
|
val handlerCollection = HandlerCollection()
|
||||||
|
|
||||||
|
// Export JMX monitoring statistics and data over REST/JSON.
|
||||||
|
if (configuration.getString("exportJMXto").split(',').contains("http")) {
|
||||||
|
val classpath = System.getProperty("java.class.path").split(System.getProperty("path.separator"))
|
||||||
|
val warpath = classpath.firstOrNull { it.contains("jolokia-agent-war-2") && it.endsWith(".war") }
|
||||||
|
if (warpath != null) {
|
||||||
|
handlerCollection.addHandler(WebAppContext().apply {
|
||||||
|
// Find the jolokia WAR file on the classpath.
|
||||||
|
contextPath = "/monitoring/json"
|
||||||
|
setInitParameter("mimeType", "application/json")
|
||||||
|
war = warpath
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
log.warn("Unable to locate Jolokia WAR on classpath")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API, data upload and download to services (attachments, rates oracles etc)
|
||||||
|
handlerCollection.addHandler(buildServletContextHandler(localRpc))
|
||||||
|
|
||||||
|
val server = Server()
|
||||||
|
|
||||||
|
val connector = if (configuration.getBoolean("useHTTPS")) {
|
||||||
|
val httpsConfiguration = HttpConfiguration()
|
||||||
|
httpsConfiguration.outputBufferSize = 32768
|
||||||
|
httpsConfiguration.addCustomizer(SecureRequestCustomizer())
|
||||||
|
val sslContextFactory = SslContextFactory()
|
||||||
|
sslContextFactory.keyStorePath = sslConfig.keyStorePath.toString()
|
||||||
|
sslContextFactory.setKeyStorePassword(sslConfig.keyStorePassword)
|
||||||
|
sslContextFactory.setKeyManagerPassword(sslConfig.keyStorePassword)
|
||||||
|
sslContextFactory.setTrustStorePath(sslConfig.trustStorePath.toString())
|
||||||
|
sslContextFactory.setTrustStorePassword(sslConfig.trustStorePassword)
|
||||||
|
sslContextFactory.setExcludeProtocols("SSL.*", "TLSv1", "TLSv1.1")
|
||||||
|
sslContextFactory.setIncludeProtocols("TLSv1.2")
|
||||||
|
sslContextFactory.setExcludeCipherSuites(".*NULL.*", ".*RC4.*", ".*MD5.*", ".*DES.*", ".*DSS.*")
|
||||||
|
sslContextFactory.setIncludeCipherSuites(".*AES.*GCM.*")
|
||||||
|
val sslConnector = ServerConnector(server, SslConnectionFactory(sslContextFactory, "http/1.1"), HttpConnectionFactory(httpsConfiguration))
|
||||||
|
sslConnector.port = address.port
|
||||||
|
sslConnector
|
||||||
|
} else {
|
||||||
|
val httpConfiguration = HttpConfiguration()
|
||||||
|
httpConfiguration.outputBufferSize = 32768
|
||||||
|
val httpConnector = ServerConnector(server, HttpConnectionFactory(httpConfiguration))
|
||||||
|
httpConnector.port = address.port
|
||||||
|
httpConnector
|
||||||
|
}
|
||||||
|
server.connectors = arrayOf<Connector>(connector)
|
||||||
|
|
||||||
|
server.handler = handlerCollection
|
||||||
|
//runOnStop += Runnable { server.stop() }
|
||||||
|
server.start()
|
||||||
|
log.info("Embedded web server is listening on", "http://${InetAddress.getLocalHost().hostAddress}:${connector.port}/")
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildServletContextHandler(localRpc: CordaRPCOps): ServletContextHandler {
|
||||||
|
return ServletContextHandler().apply {
|
||||||
|
contextPath = "/"
|
||||||
|
//setAttribute("node", this@Node)
|
||||||
|
addServlet(DataUploadServlet::class.java, "/upload/*")
|
||||||
|
addServlet(AttachmentDownloadServlet::class.java, "/attachments/*")
|
||||||
|
|
||||||
|
val resourceConfig = ResourceConfig()
|
||||||
|
// Add your API provider classes (annotated for JAX-RS) here
|
||||||
|
// TODO: Remove this at cleanup time
|
||||||
|
//resourceConfig.register(Config(services))
|
||||||
|
resourceConfig.register(ResponseFilter())
|
||||||
|
// TODO: Move the API out of node and to here.
|
||||||
|
//resourceConfig.register(api)
|
||||||
|
|
||||||
|
val webAPIsOnClasspath = pluginRegistries.flatMap { x -> x.webApis }
|
||||||
|
for (webapi in webAPIsOnClasspath) {
|
||||||
|
log.info("Add plugin web API from attachment $webapi")
|
||||||
|
val customAPI = try {
|
||||||
|
webapi.apply(localRpc)
|
||||||
|
} catch (ex: InvocationTargetException) {
|
||||||
|
log.error("Constructor $webapi threw an error: ", ex.targetException)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resourceConfig.register(customAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
val staticDirMaps = pluginRegistries.map { x -> x.staticServeDirs }
|
||||||
|
val staticDirs = staticDirMaps.flatMap { it.keys }.zip(staticDirMaps.flatMap { it.values })
|
||||||
|
staticDirs.forEach {
|
||||||
|
val staticDir = ServletHolder(DefaultServlet::class.java)
|
||||||
|
staticDir.setInitParameter("resourceBase", it.second)
|
||||||
|
staticDir.setInitParameter("dirAllowed", "true")
|
||||||
|
staticDir.setInitParameter("pathInfoOnly", "true")
|
||||||
|
addServlet(staticDir, "/web/${it.first}/*")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give the app a slightly better name in JMX rather than a randomly generated one and enable JMX
|
||||||
|
resourceConfig.addProperties(mapOf(ServerProperties.APPLICATION_NAME to "node.api",
|
||||||
|
ServerProperties.MONITORING_STATISTICS_MBEANS_ENABLED to "true"))
|
||||||
|
|
||||||
|
val container = ServletContainer(resourceConfig)
|
||||||
|
val jerseyServlet = ServletHolder(container)
|
||||||
|
addServlet(jerseyServlet, "/api/*")
|
||||||
|
jerseyServlet.initOrder = 0 // Initialise at server start
|
||||||
|
|
||||||
|
// Wrap all API calls in a database transaction.
|
||||||
|
// TODO: Remove this when cleaning up
|
||||||
|
//val filterHolder = FilterHolder(Node.DatabaseTransactionFilter(database))
|
||||||
|
//addFilter(filterHolder, "/api/*", EnumSet.of(DispatcherType.REQUEST))
|
||||||
|
//addFilter(filterHolder, "/upload/*", EnumSet.of(DispatcherType.REQUEST))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connectLocalRpcAsNodeUser(): CordaRPCOps {
|
||||||
|
val client = CordaRPCClient(HostAndPort.fromString(nodeInfo.address.toString()), sslConfig)
|
||||||
|
client.start(ArtemisMessagingComponent.NODE_USER, ArtemisMessagingComponent.NODE_USER)
|
||||||
|
return client.proxy()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fetch CordaPluginRegistry classes registered in META-INF/services/net.corda.core.node.CordaPluginRegistry files that exist in the classpath */
|
||||||
|
val pluginRegistries: List<CordaPluginRegistry> by lazy {
|
||||||
|
ServiceLoader.load(CordaPluginRegistry::class.java).toList()
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ include 'finance:isolated'
|
|||||||
include 'core'
|
include 'core'
|
||||||
include 'node'
|
include 'node'
|
||||||
include 'node:capsule'
|
include 'node:capsule'
|
||||||
|
include 'node:webserver'
|
||||||
include 'client'
|
include 'client'
|
||||||
include 'experimental'
|
include 'experimental'
|
||||||
include 'experimental:sandbox'
|
include 'experimental:sandbox'
|
||||||
|
Reference in New Issue
Block a user