Merge remote-tracking branch 'open/master' into andrius/merge-06-21-2

This commit is contained in:
Andrius Dagys
2018-06-21 14:02:37 +01:00
17 changed files with 378 additions and 107 deletions

View File

@ -0,0 +1,121 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package net.corda.tools.shell
import com.jcabi.manifests.Manifests
import joptsimple.OptionException
import net.corda.core.internal.*
import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiConsole
import java.net.URL
import java.net.URLClassLoader
import java.nio.file.Path
import java.util.concurrent.CountDownLatch
import kotlin.streams.toList
import java.io.IOException
import java.io.BufferedReader
import java.io.InputStreamReader
import kotlin.system.exitProcess
fun main(args: Array<String>) {
val argsParser = CommandLineOptionParser()
val cmdlineOptions = try {
argsParser.parse(*args)
} catch (e: OptionException) {
println("Invalid command line arguments: ${e.message}")
argsParser.printHelp(System.out)
exitProcess(1)
}
if (cmdlineOptions.help) {
argsParser.printHelp(System.out)
return
}
val config = try {
cmdlineOptions.toConfig()
} catch(e: Exception) {
println("Configuration exception: ${e.message}")
exitProcess(1)
}
StandaloneShell(config).run()
}
class StandaloneShell(private val configuration: ShellConfiguration) {
private fun getCordappsInDirectory(cordappsDir: Path?): List<URL> =
if (cordappsDir == null || !cordappsDir.exists()) {
emptyList()
} else {
cordappsDir.list {
it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList()
}
}
//Workaround in case console is not available
@Throws(IOException::class)
private fun readLine(format: String, vararg args: Any): String {
if (System.console() != null) {
return System.console().readLine(format, *args)
}
print(String.format(format, *args))
val reader = BufferedReader(InputStreamReader(System.`in`))
return reader.readLine()
}
@Throws(IOException::class)
private fun readPassword(format: String, vararg args: Any) =
if (System.console() != null) System.console().readPassword(format, *args) else this.readLine(format, *args).toCharArray()
private fun getManifestEntry(key: String) = if (Manifests.exists(key)) Manifests.read(key) else "Unknown"
fun run() {
val cordappJarPaths = getCordappsInDirectory(configuration.cordappsDirectory)
val classLoader: ClassLoader = URLClassLoader(cordappJarPaths.toTypedArray(), javaClass.classLoader)
with(configuration) {
if (user.isEmpty()) {
user = readLine("User:")
}
if (password.isEmpty()) {
password = String(readPassword("Password:"))
}
}
InteractiveShell.startShell(configuration, classLoader)
try {
//connecting to node by requesting node info to fail fast
InteractiveShell.nodeInfo()
} catch (e: Exception) {
println("Cannot login to ${configuration.hostAndPort}, reason: \"${e.message}\"")
exitProcess(1)
}
val exit = CountDownLatch(1)
AnsiConsole.systemInstall()
println(Ansi.ansi().fgBrightRed().a(
""" ______ __""").newline().a(
""" / ____/ _________/ /___ _""").newline().a(
""" / / __ / ___/ __ / __ `/ """).newline().fgBrightRed().a(
"""/ /___ /_/ / / / /_/ / /_/ /""").newline().fgBrightRed().a(
"""\____/ /_/ \__,_/\__,_/""").reset().fgBrightDefault().bold()
.newline().a("--- ${getManifestEntry("Corda-Vendor")} ${getManifestEntry("Corda-Release-Version")} (${getManifestEntry("Corda-Revision").take(7)}) ---")
.newline()
.newline().a("Standalone Shell connected to ${configuration.hostAndPort}")
.reset())
InteractiveShell.runLocalShell {
exit.countDown()
}
configuration.sshdPort?.apply{ println("SSH server listening on port $this.") }
exit.await()
// because we can't clean certain Crash Shell threads that block on read()
exitProcess(0)
}
}

View File

@ -0,0 +1,217 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package net.corda.tools.shell
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import joptsimple.OptionParser
import joptsimple.util.EnumConverter
import net.corda.core.internal.div
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.nodeapi.internal.config.parseAs
import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR
import org.slf4j.event.Level
import java.io.PrintStream
import java.nio.file.Path
import java.nio.file.Paths
// NOTE: Do not use any logger in this class as args parsing is done before the logger is setup.
class CommandLineOptionParser {
private val optionParser = OptionParser()
private val configFileArg = optionParser
.accepts("config-file", "The path to the shell configuration file, used instead of providing the rest of command line options.")
.withOptionalArg()
private val cordappsDirectoryArg = optionParser
.accepts("cordpass-directory", "The path to directory containing Cordapps jars, Cordapps are require when starting flows.")
.withOptionalArg()
private val commandsDirectoryArg = optionParser
.accepts("commands-directory", "The directory with additional CrAsH shell commands.")
.withOptionalArg()
private val hostArg = optionParser
.acceptsAll(listOf("h", "host"), "The host of the Corda node.")
.withRequiredArg()
private val portArg = optionParser
.acceptsAll(listOf("p", "port"), "The port of the Corda node.")
.withRequiredArg()
private val userArg = optionParser
.accepts("user", "The RPC user name.")
.withOptionalArg()
private val passwordArg = optionParser
.accepts("password", "The RPC user password.")
.withOptionalArg()
private val loggerLevel = optionParser
.accepts("logging-level", "Enable logging at this level and higher.")
.withRequiredArg()
.withValuesConvertedBy(object : EnumConverter<Level>(Level::class.java) {})
.defaultsTo(Level.INFO)
private val sshdPortArg = optionParser
.accepts("sshd-port", "Enables SSH server for shell.")
.withOptionalArg()
private val sshdHostKeyDirectoryArg = optionParser
.accepts("sshd-hostkey-directory", "The directory with hostkey.pem file for SSH server.")
.withOptionalArg()
private val helpArg = optionParser
.accepts("help")
.forHelp()
private val trustStorePasswordArg = optionParser
.accepts("truststore-password", "The password to unlock the TrustStore file.")
.withOptionalArg()
private val trustStoreDirArg = optionParser
.accepts("truststore-file", "The path to the TrustStore file.")
.withOptionalArg()
private val trustStoreTypeArg = optionParser
.accepts("truststore-type", "The type of the TrustStore (e.g. JKS).")
.withOptionalArg()
fun parse(vararg args: String): CommandLineOptions {
val optionSet = optionParser.parse(*args)
return CommandLineOptions(
configFile = optionSet.valueOf(configFileArg),
host = optionSet.valueOf(hostArg),
port = optionSet.valueOf(portArg),
user = optionSet.valueOf(userArg),
password = optionSet.valueOf(passwordArg),
commandsDirectory = (optionSet.valueOf(commandsDirectoryArg))?.let { Paths.get(it).normalize().toAbsolutePath() },
cordappsDirectory = (optionSet.valueOf(cordappsDirectoryArg))?.let { Paths.get(it).normalize().toAbsolutePath() },
help = optionSet.has(helpArg),
loggingLevel = optionSet.valueOf(loggerLevel),
sshdPort = optionSet.valueOf(sshdPortArg),
sshdHostKeyDirectory = (optionSet.valueOf(sshdHostKeyDirectoryArg))?.let { Paths.get(it).normalize().toAbsolutePath() },
trustStorePassword = optionSet.valueOf(trustStorePasswordArg),
trustStoreFile = (optionSet.valueOf(trustStoreDirArg))?.let { Paths.get(it).normalize().toAbsolutePath() },
trustStoreType = optionSet.valueOf(trustStoreTypeArg))
}
fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink)
}
data class CommandLineOptions(val configFile: String?,
val commandsDirectory: Path?,
val cordappsDirectory: Path?,
val host: String?,
val port: String?,
val user: String?,
val password: String?,
val help: Boolean,
val loggingLevel: Level,
val sshdPort: String?,
val sshdHostKeyDirectory: Path?,
val trustStorePassword: String?,
val trustStoreFile: Path?,
val trustStoreType: String?) {
private fun toConfigFile(): Config {
val cmdOpts = mutableMapOf<String, Any?>()
commandsDirectory?.apply { cmdOpts["extensions.commands.path"] = this.toString() }
cordappsDirectory?.apply { cmdOpts["extensions.cordapps.path"] = this.toString() }
user?.apply { cmdOpts["node.user"] = this }
password?.apply { cmdOpts["node.password"] = this }
host?.apply { cmdOpts["node.addresses.rpc.host"] = this }
port?.apply { cmdOpts["node.addresses.rpc.port"] = this }
trustStoreFile?.apply { cmdOpts["ssl.truststore.path"] = this.toString() }
trustStorePassword?.apply { cmdOpts["ssl.truststore.password"] = this }
trustStoreType?.apply { cmdOpts["ssl.truststore.type"] = this }
sshdPort?.apply {
cmdOpts["extensions.sshd.port"] = this
cmdOpts["extensions.sshd.enabled"] = true
}
sshdHostKeyDirectory?.apply { cmdOpts["extensions.sshd.hostkeypath"] = this.toString() }
return ConfigFactory.parseMap(cmdOpts)
}
/** Return configuration parsed from an optional config file (provided by the command line option)
* and then overridden by the command line options */
fun toConfig(): ShellConfiguration {
val fileConfig = configFile?.let { ConfigFactory.parseFile(Paths.get(configFile).toFile()) }
?: ConfigFactory.empty()
val typeSafeConfig = toConfigFile().withFallback(fileConfig).resolve()
val shellConfigFile = typeSafeConfig.parseAs<ShellConfigurationFile.ShellConfigFile>()
return shellConfigFile.toShellConfiguration()
}
}
/** Object representation of Shell configuration file */
private class ShellConfigurationFile {
data class Rpc(
val host: String,
val port: Int)
data class Addresses(
val rpc: Rpc
)
data class Node(
val addresses: Addresses,
val user: String?,
val password: String?
)
data class Cordapps(
val path: String
)
data class Sshd(
val enabled: Boolean,
val port: Int,
val hostkeypath: String?
)
data class Commands(
val path: String
)
data class Extensions(
val cordapps: Cordapps,
val sshd: Sshd,
val commands: Commands?
)
data class KeyStore(
val path: String,
val type: String = "JKS",
val password: String
)
data class Ssl(
val truststore: KeyStore
)
data class ShellConfigFile(
val node: Node,
val extensions: Extensions?,
val ssl: Ssl?
) {
fun toShellConfiguration(): ShellConfiguration {
val sslOptions =
ssl?.let {
ClientRpcSslOptions(
trustStorePath = Paths.get(it.truststore.path),
trustStorePassword = it.truststore.password)
}
return ShellConfiguration(
commandsDirectory = extensions?.commands?.let { Paths.get(it.path) } ?: Paths.get(".")
/ COMMANDS_DIR,
cordappsDirectory = extensions?.cordapps?.let { Paths.get(it.path) },
user = node.user ?: "",
password = node.password ?: "",
hostAndPort = NetworkHostAndPort(node.addresses.rpc.host, node.addresses.rpc.port),
ssl = sslOptions,
sshdPort = extensions?.sshd?.let { if (it.enabled) it.port else null },
sshHostKeyDirectory = extensions?.sshd?.let { if (it.enabled && it.hostkeypath != null) Paths.get(it.hostkeypath) else null })
}
}
}

View File

@ -0,0 +1,193 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package net.corda.tools.shell
import net.corda.core.internal.toPath
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.messaging.ClientRpcSslOptions
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.slf4j.event.Level
import java.nio.file.Paths
import kotlin.test.assertEquals
class StandaloneShellArgsParserTest {
private val CONFIG_FILE = StandaloneShellArgsParserTest::class.java.getResource("/config.conf").toPath()
@Test
fun args_to_cmd_options() {
val args = arrayOf("--config-file", "/x/y/z/config.conf",
"--commands-directory", "/x/y/commands",
"--cordpass-directory", "/x/y/cordapps",
"--host", "alocalhost",
"--port", "1234",
"--user", "demo",
"--password", "abcd1234",
"--logging-level", "DEBUG",
"--sshd-port", "2223",
"--sshd-hostkey-directory", "/x/y/ssh",
"--help",
"--truststore-password", "pass2",
"--truststore-file", "/x/y/truststore.jks",
"--truststore-type", "dummy")
val expectedOptions = CommandLineOptions(
configFile = "/x/y/z/config.conf",
commandsDirectory = Paths.get("/x/y/commands").normalize().toAbsolutePath(),
cordappsDirectory = Paths.get("/x/y/cordapps").normalize().toAbsolutePath(),
host = "alocalhost",
port = "1234",
user = "demo",
password = "abcd1234",
help = true,
loggingLevel = Level.DEBUG,
sshdPort = "2223",
sshdHostKeyDirectory = Paths.get("/x/y/ssh").normalize().toAbsolutePath(),
trustStorePassword = "pass2",
trustStoreFile = Paths.get("/x/y/truststore.jks").normalize().toAbsolutePath(),
trustStoreType = "dummy")
val options = CommandLineOptionParser().parse(*args)
assertThat(options).isEqualTo(expectedOptions)
}
@Test
fun empty_args_to_cmd_options() {
val args = emptyArray<String>()
val expectedOptions = CommandLineOptions(configFile = null,
commandsDirectory = null,
cordappsDirectory = null,
host = null,
port = null,
user = null,
password = null,
help = false,
loggingLevel = Level.INFO,
sshdPort = null,
sshdHostKeyDirectory = null,
trustStorePassword = null,
trustStoreFile = null,
trustStoreType = null)
val options = CommandLineOptionParser().parse(*args)
assertEquals(expectedOptions, options)
}
@Test
fun args_to_config() {
val options = CommandLineOptions(configFile = null,
commandsDirectory = Paths.get("/x/y/commands"),
cordappsDirectory = Paths.get("/x/y/cordapps"),
host = "alocalhost",
port = "1234",
user = "demo",
password = "abcd1234",
help = true,
loggingLevel = Level.DEBUG,
sshdPort = "2223",
sshdHostKeyDirectory = Paths.get("/x/y/ssh"),
trustStorePassword = "pass2",
trustStoreFile = Paths.get("/x/y/truststore.jks"),
trustStoreType = "dummy"
)
val expectedSsl = ClientRpcSslOptions(
trustStorePath = Paths.get("/x/y/truststore.jks"),
trustStorePassword = "pass2")
val expectedConfig = ShellConfiguration(
commandsDirectory = Paths.get("/x/y/commands"),
cordappsDirectory = Paths.get("/x/y/cordapps"),
user = "demo",
password = "abcd1234",
hostAndPort = NetworkHostAndPort("alocalhost", 1234),
ssl = expectedSsl,
sshdPort = 2223,
sshHostKeyDirectory = Paths.get("/x/y/ssh"),
noLocalShell = false)
val config = options.toConfig()
assertEquals(expectedConfig, config)
}
@Test
fun cmd_options_to_config_from_file() {
val options = CommandLineOptions(configFile = CONFIG_FILE.toString(),
commandsDirectory = null,
cordappsDirectory = null,
host = null,
port = null,
user = null,
password = null,
help = false,
loggingLevel = Level.DEBUG,
sshdPort = null,
sshdHostKeyDirectory = null,
trustStorePassword = null,
trustStoreFile = null,
trustStoreType = null)
val expectedConfig = ShellConfiguration(
commandsDirectory = Paths.get("/x/y/commands"),
cordappsDirectory = Paths.get("/x/y/cordapps"),
user = "demo",
password = "abcd1234",
hostAndPort = NetworkHostAndPort("alocalhost", 1234),
ssl = ClientRpcSslOptions(
trustStorePath = Paths.get("/x/y/truststore.jks"),
trustStorePassword = "pass2"),
sshdPort = 2223)
val config = options.toConfig()
assertEquals(expectedConfig, config)
}
@Test
fun cmd_options_override_config_from_file() {
val options = CommandLineOptions(configFile = CONFIG_FILE.toString(),
commandsDirectory = null,
cordappsDirectory = null,
host = null,
port = null,
user = null,
password = "blabla",
help = false,
loggingLevel = Level.DEBUG,
sshdPort = null,
sshdHostKeyDirectory = null,
trustStorePassword = null,
trustStoreFile = null,
trustStoreType = null)
val expectedSsl = ClientRpcSslOptions(
trustStorePath = Paths.get("/x/y/truststore.jks"),
trustStorePassword = "pass2")
val expectedConfig = ShellConfiguration(
commandsDirectory = Paths.get("/x/y/commands"),
cordappsDirectory = Paths.get("/x/y/cordapps"),
user = "demo",
password = "blabla",
hostAndPort = NetworkHostAndPort("alocalhost", 1234),
ssl = expectedSsl,
sshdPort = 2223)
val config = options.toConfig()
assertEquals(expectedConfig, config)
}
}

View File

@ -0,0 +1,29 @@
node {
addresses {
rpc {
host : "alocalhost"
port : 1234
}
}
user : demo
password : abcd1234
}
extensions {
cordapps {
path : "/x/y/cordapps"
}
sshd {
enabled : "true"
port : 2223
}
commands {
path : /x/y/commands
}
}
ssl {
truststore {
path : "/x/y/truststore.jks"
type : "JKS"
password : "pass2"
}
}