diff --git a/webserver/build.gradle b/webserver/build.gradle index 318cf9fb13..2b21e72fa8 100644 --- a/webserver/build.gradle +++ b/webserver/build.gradle @@ -56,6 +56,9 @@ dependencies { compile "org.glassfish.jersey.containers:jersey-container-jetty-http:$jersey_version" compile "org.glassfish.jersey.media:jersey-media-json-jackson:$jersey_version" + // For rendering the index page. + compile "org.jetbrains.kotlinx:kotlinx-html-jvm:0.6.3" + testCompile "junit:junit:$junit_version" } diff --git a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt index 8989f70e87..33dd19ea46 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt @@ -7,10 +7,7 @@ import net.corda.core.node.CordaPluginRegistry import net.corda.core.utilities.loggerFor import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.webserver.WebServerConfig -import net.corda.webserver.servlets.AttachmentDownloadServlet -import net.corda.webserver.servlets.DataUploadServlet -import net.corda.webserver.servlets.ObjectMapperConfig -import net.corda.webserver.servlets.ResponseFilter +import net.corda.webserver.servlets.* import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException import org.eclipse.jetty.server.* import org.eclipse.jetty.server.handler.ErrorHandler @@ -159,13 +156,13 @@ class NodeWebServer(val config: WebServerConfig) { addServlet(staticDir, "/web/${it.first}/*") } - // If we have at least one static web data directory, redirect / to the right URL. - staticDirs.firstOrNull()?.let { addServlet(ServletHolder(IndexRedirectServlet("/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 infoServlet = ServletHolder(CorDappInfoServlet(pluginRegistries, localRpc)) + addServlet(infoServlet, "") + val container = ServletContainer(resourceConfig) val jerseyServlet = ServletHolder(container) addServlet(jerseyServlet, "/api/*") @@ -173,12 +170,6 @@ class NodeWebServer(val config: WebServerConfig) { } } - private inner class IndexRedirectServlet(private val redirectTo: String) : HttpServlet() { - override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { - resp.sendRedirect(resp.encodeRedirectURL(redirectTo)) - } - } - private fun retryConnectLocalRpc(): CordaRPCOps { while (true) { try { diff --git a/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt b/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt new file mode 100644 index 0000000000..3583498abf --- /dev/null +++ b/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt @@ -0,0 +1,90 @@ +package net.corda.webserver.servlets + +import kotlinx.html.* +import kotlinx.html.stream.appendHTML +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.node.CordaPluginRegistry +import org.glassfish.jersey.server.model.Resource +import org.glassfish.jersey.server.model.ResourceMethod +import javax.servlet.http.HttpServlet +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +/** + * Dumps some data about the installed CorDapps. + * TODO: Add registered flow initiators. + */ +class CorDappInfoServlet(val plugins: List, val rpc: CordaRPCOps): HttpServlet() { + + override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { + resp.writer.appendHTML().html { + head { + title { +"Installed CorDapps" } + } + body { + h2 { +"Installed CorDapps" } + plugins.forEach { plugin -> + h3 { +plugin::class.java.name } + if (plugin.requiredFlows.isNotEmpty()) { + div { + p { +"Whitelisted flows:" } + ul { + plugin.requiredFlows.map { it.key }.forEach { li { +it } } + } + } + } + if (plugin.webApis.isNotEmpty()) { + div { + plugin.webApis.forEach { api -> + val resource = Resource.from(api.apply(rpc)::class.java) + p { +"${resource.name}:" } + val endpoints = processEndpoints("", resource, mutableListOf()) + ul { + endpoints.forEach { + li { a(it.uri) { +"${it.method}\t${it.text}" } } + } + } + } + } + } + if (plugin.staticServeDirs.isNotEmpty()) { + div { + p { +"Static web content:" } + ul { + plugin.staticServeDirs.map { it.key }.forEach { + li { a("web/$it") { +it } } + } + } + } + } + } + } + } + } + + data class Endpoint(val method: String, val uri: String, val text: String) + + /** + * Recursively enumerate and record all of the end-points listed in the API implementations. + */ + private fun processEndpoints(uriPrefix: String, resource: Resource, endpoints: MutableList): List { + val resources = arrayListOf() + val path = if (resource.path != null) "$uriPrefix/${resource.path}" else uriPrefix + + resources.addAll(resource.childResources) + + for (method in resource.allMethods) { + if (method.type == ResourceMethod.JaxrsType.SUB_RESOURCE_LOCATOR) { + resources.add( Resource.from(resource.resourceLocator.invocable.definitionMethod.returnType)) + } else { + endpoints.add(Endpoint(method.httpMethod, "api$path", resource.path)) + } + } + + resources.forEach { + processEndpoints(path, it, endpoints) + } + + return endpoints + } +}