mirror of
https://github.com/corda/corda.git
synced 2025-06-18 07:08:15 +00:00
Added webserver module. Moved webserver files to the webserver module.
This commit is contained in:
committed by
Clinton Alexander
parent
85243a6b76
commit
bc9f86905c
@ -1,181 +0,0 @@
|
||||
package net.corda.node.webserver
|
||||
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.printBasicNodeInfo
|
||||
import net.corda.node.services.config.FullNodeConfiguration
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent
|
||||
import net.corda.node.services.messaging.CordaRPCClient
|
||||
import net.corda.node.webserver.internal.APIServerImpl
|
||||
import net.corda.node.webserver.servlets.AttachmentDownloadServlet
|
||||
import net.corda.node.webserver.servlets.DataUploadServlet
|
||||
import net.corda.node.webserver.servlets.ObjectMapperConfig
|
||||
import net.corda.node.webserver.servlets.ResponseFilter
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
|
||||
import org.eclipse.jetty.server.*
|
||||
import org.eclipse.jetty.server.handler.HandlerCollection
|
||||
import org.eclipse.jetty.servlet.DefaultServlet
|
||||
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.util.*
|
||||
|
||||
// TODO: Split into a separate module under client that packages into WAR formats.
|
||||
class WebServer(val config: FullNodeConfiguration) {
|
||||
private companion object {
|
||||
val log = loggerFor<WebServer>()
|
||||
val retryDelay = 1000L // Milliseconds
|
||||
}
|
||||
|
||||
val address = config.webAddress
|
||||
private lateinit var server: Server
|
||||
|
||||
fun start() {
|
||||
printBasicNodeInfo("Starting as webserver: ${config.webAddress}")
|
||||
server = initWebServer(retryConnectLocalRpc())
|
||||
}
|
||||
|
||||
fun run() {
|
||||
while (server.isRunning) {
|
||||
Thread.sleep(100) // TODO: Redesign
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
// TODO: Move back into the node itself.
|
||||
// Export JMX monitoring statistics and data over REST/JSON.
|
||||
if (config.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 (config.useHTTPS) {
|
||||
val httpsConfiguration = HttpConfiguration()
|
||||
httpsConfiguration.outputBufferSize = 32768
|
||||
httpsConfiguration.addCustomizer(SecureRequestCustomizer())
|
||||
val sslContextFactory = SslContextFactory()
|
||||
sslContextFactory.keyStorePath = config.keyStoreFile.toString()
|
||||
sslContextFactory.setKeyStorePassword(config.keyStorePassword)
|
||||
sslContextFactory.setKeyManagerPassword(config.keyStorePassword)
|
||||
sslContextFactory.setTrustStorePath(config.trustStoreFile.toString())
|
||||
sslContextFactory.setTrustStorePassword(config.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
|
||||
server.start()
|
||||
log.info("Started webserver on address $address")
|
||||
return server
|
||||
}
|
||||
|
||||
private fun buildServletContextHandler(localRpc: CordaRPCOps): ServletContextHandler {
|
||||
return ServletContextHandler().apply {
|
||||
contextPath = "/"
|
||||
setAttribute("rpc", localRpc)
|
||||
addServlet(DataUploadServlet::class.java, "/upload/*")
|
||||
addServlet(AttachmentDownloadServlet::class.java, "/attachments/*")
|
||||
|
||||
val resourceConfig = ResourceConfig()
|
||||
resourceConfig.register(ObjectMapperConfig(localRpc))
|
||||
resourceConfig.register(ResponseFilter())
|
||||
resourceConfig.register(APIServerImpl(localRpc))
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private fun retryConnectLocalRpc(): CordaRPCOps {
|
||||
while (true) {
|
||||
try {
|
||||
return connectLocalRpcAsNodeUser()
|
||||
} catch (e: ActiveMQNotConnectedException) {
|
||||
log.debug("Could not connect to ${config.artemisAddress} due to exception: ", e)
|
||||
Thread.sleep(retryDelay)
|
||||
// This error will happen if the server has yet to create the keystore
|
||||
// Keep the fully qualified package name due to collisions with the Kotlin stdlib
|
||||
// exception of the same name
|
||||
} catch (e: java.nio.file.NoSuchFileException) {
|
||||
log.debug("Tried to open a file that doesn't yet exist, retrying", e)
|
||||
Thread.sleep(retryDelay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectLocalRpcAsNodeUser(): CordaRPCOps {
|
||||
log.info("Connecting to node at ${config.artemisAddress} as node user")
|
||||
val client = CordaRPCClient(config.artemisAddress, config)
|
||||
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()
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package net.corda.node.webserver.api
|
||||
|
||||
import net.corda.core.node.NodeInfo
|
||||
import java.time.LocalDateTime
|
||||
import javax.ws.rs.GET
|
||||
import javax.ws.rs.Path
|
||||
import javax.ws.rs.Produces
|
||||
import javax.ws.rs.core.MediaType
|
||||
import javax.ws.rs.core.Response
|
||||
|
||||
/**
|
||||
* Top level interface to external interaction with the distributed ledger.
|
||||
*
|
||||
* Wherever a list is returned by a fetchXXX method that corresponds with an input list, that output list will have optional elements
|
||||
* where a null indicates "missing" and the elements returned will be in the order corresponding with the input list.
|
||||
*
|
||||
*/
|
||||
@Path("")
|
||||
interface APIServer {
|
||||
/**
|
||||
* Report current UTC time as understood by the platform.
|
||||
*/
|
||||
@GET
|
||||
@Path("servertime")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
fun serverTime(): LocalDateTime
|
||||
|
||||
/**
|
||||
* Report whether this node is started up or not.
|
||||
*/
|
||||
@GET
|
||||
@Path("status")
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
fun status(): Response
|
||||
|
||||
/**
|
||||
* Report this node's configuration and identities.
|
||||
*/
|
||||
@GET
|
||||
@Path("info")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
fun info(): NodeInfo
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package net.corda.node.webserver.api
|
||||
|
||||
/**
|
||||
* Extremely rudimentary query language which should most likely be replaced with a product.
|
||||
*/
|
||||
interface StatesQuery {
|
||||
companion object {
|
||||
fun select(criteria: Criteria): Selection {
|
||||
return Selection(criteria)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO make constructors private
|
||||
data class Selection(val criteria: Criteria) : StatesQuery
|
||||
|
||||
interface Criteria {
|
||||
object AllDeals : Criteria
|
||||
|
||||
data class Deal(val ref: String) : Criteria
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package net.corda.node.webserver.internal
|
||||
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.node.webserver.api.*
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import javax.ws.rs.core.Response
|
||||
|
||||
class APIServerImpl(val rpcOps: CordaRPCOps) : APIServer {
|
||||
|
||||
override fun serverTime(): LocalDateTime {
|
||||
return LocalDateTime.ofInstant(rpcOps.currentNodeTime(), ZoneId.of("UTC"))
|
||||
}
|
||||
|
||||
/**
|
||||
* This endpoint is for polling if the webserver is serving. It will always return 200.
|
||||
*/
|
||||
override fun status(): Response {
|
||||
return Response.ok("started").build()
|
||||
}
|
||||
|
||||
override fun info() = rpcOps.nodeIdentity()
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package net.corda.node.webserver.servlets
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.node.services.StorageService
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.io.FileNotFoundException
|
||||
import javax.servlet.http.HttpServlet
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
/**
|
||||
* Allows the node administrator to either download full attachment zips, or individual files within those zips.
|
||||
*
|
||||
* GET /attachments/123abcdef12121 -> download the zip identified by this hash
|
||||
* GET /attachments/123abcdef12121/foo.txt -> download that file specifically
|
||||
*
|
||||
* Files are always forced to be downloads, they may not be embedded into web pages for security reasons.
|
||||
*
|
||||
* TODO: See if there's a way to prevent access by JavaScript.
|
||||
* TODO: Provide an endpoint that exposes attachment file listings, to make attachments browseable.
|
||||
*/
|
||||
class AttachmentDownloadServlet : HttpServlet() {
|
||||
private val log = loggerFor<AttachmentDownloadServlet>()
|
||||
|
||||
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
|
||||
val reqPath = req.pathInfo?.substring(1)
|
||||
if (reqPath == null) {
|
||||
resp.sendError(HttpServletResponse.SC_BAD_REQUEST)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val hash = SecureHash.parse(reqPath.substringBefore('/'))
|
||||
val storage = servletContext.getAttribute("storage") as StorageService
|
||||
val attachment = storage.attachments.openAttachment(hash) ?: throw FileNotFoundException()
|
||||
|
||||
// Don't allow case sensitive matches inside the jar, it'd just be confusing.
|
||||
val subPath = reqPath.substringAfter('/', missingDelimiterValue = "").toLowerCase()
|
||||
|
||||
resp.contentType = "application/octet-stream"
|
||||
if (subPath == "") {
|
||||
resp.addHeader("Content-Disposition", "attachment; filename=\"$hash.zip\"")
|
||||
attachment.open().use { it.copyTo(resp.outputStream) }
|
||||
} else {
|
||||
val filename = subPath.split('/').last()
|
||||
resp.addHeader("Content-Disposition", "attachment; filename=\"$filename\"")
|
||||
attachment.extractFile(subPath, resp.outputStream)
|
||||
}
|
||||
resp.outputStream.close()
|
||||
} catch(e: FileNotFoundException) {
|
||||
log.warn("404 Not Found whilst trying to handle attachment download request for ${servletContext.contextPath}/$reqPath")
|
||||
resp.sendError(HttpServletResponse.SC_NOT_FOUND)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package net.corda.node.webserver.servlets
|
||||
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.services.api.AcceptsFileUpload
|
||||
import org.apache.commons.fileupload.servlet.ServletFileUpload
|
||||
import java.util.*
|
||||
import javax.servlet.http.HttpServlet
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
/**
|
||||
* Accepts binary streams, finds the right [AcceptsFileUpload] implementor and hands the stream off to it.
|
||||
*/
|
||||
class DataUploadServlet: HttpServlet() {
|
||||
private val log = loggerFor<DataUploadServlet>()
|
||||
|
||||
override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) {
|
||||
@Suppress("DEPRECATION") // Bogus warning due to superclass static method being deprecated.
|
||||
val isMultipart = ServletFileUpload.isMultipartContent(req)
|
||||
val rpc = servletContext.getAttribute("rpc") as CordaRPCOps
|
||||
|
||||
if (!isMultipart) {
|
||||
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "This end point is for data uploads only.")
|
||||
return
|
||||
}
|
||||
|
||||
val upload = ServletFileUpload()
|
||||
val iterator = upload.getItemIterator(req)
|
||||
val messages = ArrayList<String>()
|
||||
|
||||
if (!iterator.hasNext()) {
|
||||
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Got an upload request with no files")
|
||||
return
|
||||
}
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
val item = iterator.next()
|
||||
log.info("Receiving ${item.name}")
|
||||
|
||||
try {
|
||||
val dataType = req.pathInfo.substring(1).substringBefore('/')
|
||||
messages += rpc.uploadFile(dataType, item.name, item.openStream())
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// Send back the hashes as a convenience for the user.
|
||||
val writer = resp.writer
|
||||
messages.forEach { writer.println(it) }
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package net.corda.node.webserver.servlets
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.node.utilities.JsonSupport
|
||||
import javax.ws.rs.ext.ContextResolver
|
||||
import javax.ws.rs.ext.Provider
|
||||
|
||||
/**
|
||||
* Primary purpose is to install Kotlin extensions for Jackson ObjectMapper so data classes work
|
||||
* and to organise serializers / deserializers for java.time.* classes as necessary.
|
||||
*/
|
||||
@Provider
|
||||
class ObjectMapperConfig(rpc: CordaRPCOps) : ContextResolver<ObjectMapper> {
|
||||
val defaultObjectMapper = JsonSupport.createDefaultMapper(rpc)
|
||||
override fun getContext(type: Class<*>) = defaultObjectMapper
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package net.corda.node.webserver.servlets
|
||||
|
||||
import javax.ws.rs.container.ContainerRequestContext
|
||||
import javax.ws.rs.container.ContainerResponseContext
|
||||
import javax.ws.rs.container.ContainerResponseFilter
|
||||
import javax.ws.rs.ext.Provider
|
||||
|
||||
/**
|
||||
* This adds headers needed for cross site scripting on API clients.
|
||||
*/
|
||||
@Provider
|
||||
class ResponseFilter : ContainerResponseFilter {
|
||||
override fun filter(requestContext: ContainerRequestContext, responseContext: ContainerResponseContext) {
|
||||
val headers = responseContext.headers
|
||||
|
||||
/**
|
||||
* TODO we need to revisit this for security reasons
|
||||
*
|
||||
* We don't want this scriptable from any web page anywhere, but for demo reasons
|
||||
* we're making this really easy to access pending a proper security approach including
|
||||
* access control and authentication at a network and software level.
|
||||
*
|
||||
*/
|
||||
headers.add("Access-Control-Allow-Origin", "*")
|
||||
|
||||
if (requestContext.method == "OPTIONS") {
|
||||
headers.add("Access-Control-Allow-Headers", "Content-Type,Accept,Origin")
|
||||
headers.add("Access-Control-Allow-Methods", "POST,PUT,GET,OPTIONS")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user