mirror of
https://github.com/corda/corda.git
synced 2025-02-02 17:21:06 +00:00
Webserver: Redirect / to the first static web path.
DemoBench: Misc usability improvements: - Pre-fill details for some fictional banks when Add Node is pushed. - Make services a checkbox list rather than one where you have to know how to use the keyboard to do multi-select. - Make web server launch button spin until server is launched to show activity. - Suppress an exception that spams the log due to inability to load all the states. It'll get fixed as part of the vault API and serialisation work.
This commit is contained in:
parent
35f6de9c50
commit
238d4e29e2
@ -9,12 +9,17 @@ import javax.security.auth.x500.X500Principal
|
||||
|
||||
/**
|
||||
* The validation function will validate the input string using the following rules:
|
||||
*
|
||||
* - No blacklisted words like "node", "server".
|
||||
* - Restrict names to Latin scripts for now to avoid right-to-left issues, debugging issues when we can't pronounce names over the phone, and character confusability attacks.
|
||||
* - Should start with a capital letter.
|
||||
* - Restrict names to Latin scripts for now to avoid right-to-left issues, debugging issues when we can't pronounce
|
||||
* names over the phone, and character confusability attacks.
|
||||
* - Must consist of at least three letters and should start with a capital letter.
|
||||
* - No commas or equals signs.
|
||||
* - No dollars or quote marks, we might need to relax the quote mark constraint in future to handle Irish company names.
|
||||
*
|
||||
* @throws IllegalArgumentException if the name does not meet the required rules. The message indicates why not.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun validateLegalName(normalizedLegalName: String) {
|
||||
rules.forEach { it.validate(normalizedLegalName) }
|
||||
}
|
||||
@ -36,7 +41,8 @@ private val rules: List<Rule<String>> = listOf(
|
||||
// TODO: Implement confusable character detection if we add more scripts.
|
||||
UnicodeRangeRule(LATIN, COMMON, INHERITED),
|
||||
CapitalLetterRule(),
|
||||
X500NameRule()
|
||||
X500NameRule(),
|
||||
MustHaveAtLeastTwoLettersRule()
|
||||
)
|
||||
|
||||
private class UnicodeNormalizationRule : Rule<String> {
|
||||
@ -52,9 +58,9 @@ private class UnicodeRangeRule(vararg supportScripts: Character.UnicodeScript) :
|
||||
require(pattern.matcher(legalName).matches()) {
|
||||
val illegalChars = legalName.replace(pattern.toRegex(), "").toSet()
|
||||
if (illegalChars.size > 1) {
|
||||
"Illegal characters $illegalChars in \"$legalName\"."
|
||||
"Forbidden characters $illegalChars in \"$legalName\"."
|
||||
} else {
|
||||
"Illegal character $illegalChars in \"$legalName\"."
|
||||
"Forbidden character $illegalChars in \"$legalName\"."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -63,7 +69,7 @@ private class UnicodeRangeRule(vararg supportScripts: Character.UnicodeScript) :
|
||||
private class CharacterRule(vararg val bannedChars: Char) : Rule<String> {
|
||||
override fun validate(legalName: String) {
|
||||
bannedChars.forEach {
|
||||
require(!legalName.contains(it, true)) { "Illegal character: $it" }
|
||||
require(!legalName.contains(it, true)) { "Character not allowed in legal names: $it" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -71,7 +77,7 @@ private class CharacterRule(vararg val bannedChars: Char) : Rule<String> {
|
||||
private class WordRule(vararg val bannedWords: String) : Rule<String> {
|
||||
override fun validate(legalName: String) {
|
||||
bannedWords.forEach {
|
||||
require(!legalName.contains(it, ignoreCase = true)) { "Illegal word: $it" }
|
||||
require(!legalName.contains(it, ignoreCase = true)) { "Word not allowed in legal names: $it" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -84,7 +90,7 @@ private class LengthRule(val maxLength: Int) : Rule<String> {
|
||||
|
||||
private class CapitalLetterRule : Rule<String> {
|
||||
override fun validate(legalName: String) {
|
||||
val capitalizedLegalName = legalName.split(" ").map(String::capitalize).joinToString(" ")
|
||||
val capitalizedLegalName = legalName.capitalize()
|
||||
require(legalName == capitalizedLegalName) { "Legal name should be capitalized. i.e. '$capitalizedLegalName'" }
|
||||
}
|
||||
}
|
||||
@ -96,6 +102,13 @@ private class X500NameRule : Rule<String> {
|
||||
}
|
||||
}
|
||||
|
||||
private class MustHaveAtLeastTwoLettersRule : Rule<String> {
|
||||
override fun validate(legalName: String) {
|
||||
// Try to exclude names like "/", "£", "X" etc.
|
||||
require(legalName.count { it.isLetter() } >= 3) { "Must have at least two letters" }
|
||||
}
|
||||
}
|
||||
|
||||
private interface Rule<in T> {
|
||||
fun validate(legalName: T)
|
||||
}
|
||||
|
@ -81,19 +81,6 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() {
|
||||
|
||||
val nextPort: Int get() = port.andIncrement
|
||||
|
||||
fun isPortAvailable(port: Int): Boolean {
|
||||
if (isPortValid(port)) {
|
||||
try {
|
||||
ServerSocket(port).close()
|
||||
return true
|
||||
} catch (e: IOException) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun isPortValid(port: Int) = (port >= minPort) && (port <= maxPort)
|
||||
|
||||
fun keyExists(key: String) = nodes.keys.contains(key)
|
||||
|
@ -7,8 +7,27 @@ import javafx.beans.property.SimpleStringProperty
|
||||
import net.corda.core.node.CityDatabase
|
||||
import tornadofx.*
|
||||
|
||||
class NodeData {
|
||||
object SuggestedDetails {
|
||||
val banks = listOf(
|
||||
// Mike: Rome? Why Rome?
|
||||
// Roger: Notaries public (also called "notaries", "notarial officers", or "public notaries") hold an office
|
||||
// which can trace its origins back to the ancient Roman Republic, when they were called scribae ("scribes"),
|
||||
// tabelliones forenses, or personae publicae.[4]
|
||||
// Mike: Can't argue with that. It's even got a citation.
|
||||
"Notary" to "Rome",
|
||||
"Bank of Breakfast Tea" to "Liverpool",
|
||||
"Bank of Big Apples" to "New York",
|
||||
"Bank of Baguettes" to "Paris",
|
||||
"Bank of Fondue" to "Geneve",
|
||||
"Bank of Maple Syrup" to "Toronto"
|
||||
)
|
||||
|
||||
private var cursor = 0
|
||||
|
||||
val nextBank: Pair<String, String> get() = banks[cursor++ % banks.size]
|
||||
}
|
||||
|
||||
class NodeData {
|
||||
val legalName = SimpleStringProperty("")
|
||||
val nearestCity = SimpleObjectProperty(CityDatabase["London"]!!)
|
||||
val p2pPort = SimpleIntegerProperty()
|
||||
@ -16,5 +35,13 @@ class NodeData {
|
||||
val webPort = SimpleIntegerProperty()
|
||||
val h2Port = SimpleIntegerProperty()
|
||||
val extraServices = SimpleListProperty(mutableListOf<String>().observable())
|
||||
|
||||
}
|
||||
|
||||
class NodeDataModel : ItemViewModel<NodeData>(NodeData()) {
|
||||
val legalName = bind { item?.legalName }
|
||||
val nearestCity = bind { item?.nearestCity }
|
||||
val p2pPort = bind { item?.p2pPort }
|
||||
val rpcPort = bind { item?.rpcPort }
|
||||
val webPort = bind { item?.webPort }
|
||||
val h2Port = bind { item?.h2Port }
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import tornadofx.*
|
||||
|
||||
class NodeDataModel : ItemViewModel<NodeData>(NodeData()) {
|
||||
|
||||
val legalName = bind { item?.legalName }
|
||||
val nearestCity = bind { item?.nearestCity }
|
||||
val p2pPort = bind { item?.p2pPort }
|
||||
val rpcPort = bind { item?.rpcPort }
|
||||
val webPort = bind { item?.webPort }
|
||||
val h2Port = bind { item?.h2Port }
|
||||
|
||||
}
|
@ -4,9 +4,9 @@ import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
||||
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
|
||||
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory
|
||||
import javafx.application.Platform
|
||||
import javafx.beans.InvalidationListener
|
||||
import javafx.geometry.Pos
|
||||
import javafx.scene.control.ComboBox
|
||||
import javafx.scene.control.SelectionMode.MULTIPLE
|
||||
import javafx.scene.image.Image
|
||||
import javafx.scene.image.ImageView
|
||||
import javafx.scene.input.KeyCode
|
||||
@ -14,13 +14,20 @@ import javafx.scene.layout.Pane
|
||||
import javafx.scene.layout.Priority
|
||||
import javafx.stage.FileChooser
|
||||
import javafx.util.StringConverter
|
||||
import net.corda.core.div
|
||||
import net.corda.core.exists
|
||||
import net.corda.core.node.CityDatabase
|
||||
import net.corda.core.node.PhysicalLocation
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.readAllLines
|
||||
import net.corda.core.utilities.normaliseLegalName
|
||||
import net.corda.core.utilities.validateLegalName
|
||||
import net.corda.core.writeLines
|
||||
import net.corda.demobench.model.*
|
||||
import net.corda.demobench.ui.CloseableTab
|
||||
import org.controlsfx.control.CheckListView
|
||||
import tornadofx.*
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
|
||||
class NodeTabView : Fragment() {
|
||||
@ -31,9 +38,27 @@ class NodeTabView : Fragment() {
|
||||
|
||||
private companion object : Component() {
|
||||
const val textWidth = 465.0
|
||||
const val maxNameLength = 15
|
||||
|
||||
val jvm by inject<JVMConfig>()
|
||||
val cordappPathsFile = jvm.dataHome / "cordapp-paths.txt"
|
||||
|
||||
fun loadDefaultCordappPaths(): MutableList<Path> {
|
||||
if (cordappPathsFile.exists())
|
||||
return cordappPathsFile.readAllLines().map { Paths.get(it) }.filter { it.exists() }.toMutableList()
|
||||
else
|
||||
return ArrayList()
|
||||
}
|
||||
|
||||
// This is shared between tabs.
|
||||
private val cordapps = loadDefaultCordappPaths().observable()
|
||||
|
||||
init {
|
||||
// Save when the list is changed.
|
||||
cordapps.addListener(InvalidationListener {
|
||||
log.info("Writing cordapp paths to $cordappPathsFile")
|
||||
cordappPathsFile.writeLines(cordapps.map { it.toAbsolutePath().toString() })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private val nodeController by inject<NodeController>()
|
||||
@ -41,7 +66,6 @@ class NodeTabView : Fragment() {
|
||||
private val chooser = FileChooser()
|
||||
|
||||
private val model = NodeDataModel()
|
||||
private val cordapps = LinkedList<Path>().observable()
|
||||
private val availableServices: List<String> = if (nodeController.hasNetworkMap()) serviceController.services else serviceController.notaries
|
||||
|
||||
private val nodeTerminalView = find<NodeTerminalView>()
|
||||
@ -73,17 +97,34 @@ class NodeTabView : Fragment() {
|
||||
}
|
||||
key.consume()
|
||||
}
|
||||
cellCache { item ->
|
||||
hbox {
|
||||
label(item.fileName.toString())
|
||||
pane {
|
||||
hboxConstraints { hgrow = Priority.ALWAYS }
|
||||
}
|
||||
val delete = FontAwesomeIconView(FontAwesomeIcon.MINUS_CIRCLE)
|
||||
delete.setOnMouseClicked {
|
||||
cordapps.remove(selectionModel.selectedItem)
|
||||
}
|
||||
delete.style += "; -fx-cursor: hand"
|
||||
addChildIfPossible(delete)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fieldset("Services") {
|
||||
styleClass.addAll("services-panel")
|
||||
|
||||
listview(availableServices.observable()) {
|
||||
val servicesList = CheckListView(availableServices.observable()).apply {
|
||||
vboxConstraints { vGrow = Priority.ALWAYS }
|
||||
selectionModel.selectionMode = MULTIPLE
|
||||
model.item.extraServices.set(selectionModel.selectedItems)
|
||||
model.item.extraServices.set(checkModel.checkedItems)
|
||||
if (!nodeController.hasNetworkMap()) {
|
||||
checkModel.check(0)
|
||||
}
|
||||
}
|
||||
add(servicesList)
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,13 +172,15 @@ class NodeTabView : Fragment() {
|
||||
root.add(nodeConfigView)
|
||||
root.add(nodeTerminalView)
|
||||
|
||||
model.legalName.value = if (nodeController.hasNetworkMap()) "" else DUMMY_NOTARY.name
|
||||
model.nearestCity.value = if (nodeController.hasNetworkMap()) null else CityDatabase["London"]!!
|
||||
model.p2pPort.value = nodeController.nextPort
|
||||
model.rpcPort.value = nodeController.nextPort
|
||||
model.webPort.value = nodeController.nextPort
|
||||
model.h2Port.value = nodeController.nextPort
|
||||
|
||||
val defaults = SuggestedDetails.nextBank
|
||||
model.legalName.value = defaults.first
|
||||
model.nearestCity.value = CityDatabase[defaults.second]
|
||||
|
||||
chooser.title = "CorDapps"
|
||||
chooser.initialDirectory = jvm.dataHome.toFile()
|
||||
chooser.extensionFilters.add(FileChooser.ExtensionFilter("CorDapps (*.jar)", "*.jar", "*.JAR"))
|
||||
@ -151,15 +194,11 @@ class NodeTabView : Fragment() {
|
||||
if (it == null) {
|
||||
error("Node name is required")
|
||||
} else {
|
||||
val name = it.trim()
|
||||
if (name.isEmpty()) {
|
||||
error("Node name is required")
|
||||
} else if (nodeController.nameExists(name)) {
|
||||
error("Node with this name already exists")
|
||||
} else if (name.length > maxNameLength) {
|
||||
error("Name is too long")
|
||||
} else {
|
||||
try {
|
||||
validateLegalName(normaliseLegalName(it))
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
error(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import javafx.application.Platform
|
||||
import javafx.embed.swing.SwingNode
|
||||
import javafx.scene.control.Button
|
||||
import javafx.scene.control.Label
|
||||
import javafx.scene.control.ProgressIndicator
|
||||
import javafx.scene.image.ImageView
|
||||
import javafx.scene.layout.StackPane
|
||||
import javafx.scene.layout.HBox
|
||||
@ -142,10 +143,17 @@ class NodeTerminalView : Fragment() {
|
||||
return@setOnAction
|
||||
}
|
||||
launchWebButton.isDisable = true
|
||||
val oldLabel = launchWebButton.text
|
||||
launchWebButton.text = ""
|
||||
launchWebButton.graphic = ProgressIndicator()
|
||||
|
||||
log.info("Starting web server for ${config.legalName}")
|
||||
webServer.open(config) then {
|
||||
Platform.runLater { launchWebButton.isDisable = false }
|
||||
Platform.runLater {
|
||||
launchWebButton.isDisable = false
|
||||
launchWebButton.text = oldLabel
|
||||
launchWebButton.graphic = null
|
||||
}
|
||||
} success {
|
||||
log.info("Web server for ${config.legalName} started on $it")
|
||||
Platform.runLater {
|
||||
@ -204,6 +212,8 @@ class NodeTerminalView : Fragment() {
|
||||
Platform.runLater {
|
||||
balance.value = if (cashBalances.isNullOrEmpty()) "0" else cashBalances
|
||||
}
|
||||
} catch (e: ClassNotFoundException) {
|
||||
// TODO: Remove this special case once Rick's serialisation work means we can deserialise states that weren't on our own classpath.
|
||||
} catch (e: Exception) {
|
||||
log.log(Level.WARNING, "Cash balance RPC failed: ${e.message}", e)
|
||||
}
|
||||
|
@ -26,7 +26,7 @@
|
||||
<Insets left="20" top="15.0"/>
|
||||
</StackPane.margin>
|
||||
</ImageView>
|
||||
<Button fx:id="addNodeButton" mnemonicParsing="false" styleClass="info-button" text="Add Node" StackPane.alignment="TOP_RIGHT">
|
||||
<Button fx:id="addNodeButton" styleClass="info-button" text="_Add Node" StackPane.alignment="TOP_RIGHT">
|
||||
<StackPane.margin>
|
||||
<Insets right="25.0" top="10.0" />
|
||||
</StackPane.margin>
|
||||
|
@ -28,7 +28,9 @@ import java.io.IOException
|
||||
import java.io.Writer
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.util.ServiceLoader
|
||||
import javax.servlet.http.HttpServlet
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
import javax.ws.rs.core.MediaType
|
||||
|
||||
class NodeWebServer(val config: WebServerConfig) {
|
||||
@ -157,6 +159,8 @@ class NodeWebServer(val config: WebServerConfig) {
|
||||
addServlet(staticDir, "/web/${it.first}/*")
|
||||
}
|
||||
|
||||
addServlet(ServletHolder(HelpServlet("/web/${staticDirs.first().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"))
|
||||
@ -168,6 +172,12 @@ class NodeWebServer(val config: WebServerConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
private inner class HelpServlet(private val redirectTo: String) : HttpServlet() {
|
||||
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
|
||||
resp.sendRedirect(resp.encodeRedirectURL(redirectTo))
|
||||
}
|
||||
}
|
||||
|
||||
private fun retryConnectLocalRpc(): CordaRPCOps {
|
||||
while (true) {
|
||||
try {
|
||||
|
Loading…
x
Reference in New Issue
Block a user