Open Source
This commit is contained in:
Nick Dunstone
2019-12-03 10:28:00 +00:00
committed by Rick Parker
parent 96e6313fbd
commit e6f9b46584
18 changed files with 156 additions and 44 deletions

View File

@ -55,7 +55,7 @@ public class FlowShellCommand extends InteractiveShellCommand {
ANSIProgressRenderer ansiProgressRenderer,
ObjectMapper om) {
if (name == null) {
out.println("You must pass a name for the flow, see 'man flow'", Color.red);
out.println("You must pass a name for the flow, see 'man flow'", Decoration.bold, Color.red);
return;
}
String inp = input == null ? "" : String.join(" ", input).trim();

View File

@ -8,6 +8,7 @@ import org.crsh.cli.Command;
import org.crsh.cli.Man;
import org.crsh.cli.Usage;
import org.crsh.text.Color;
import org.crsh.text.Decoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -27,7 +28,7 @@ public class HashLookupShellCommand extends InteractiveShellCommand {
logger.info("Executing command \"hash-lookup\".");
if (txIdHash == null) {
out.println("Please provide a hexadecimal transaction Id hash value, see 'man hash-lookup'", Color.red);
out.println("Please provide a hexadecimal transaction Id hash value, see 'man hash-lookup'", Decoration.bold, Color.red);
return;
}
@ -38,7 +39,7 @@ public class HashLookupShellCommand extends InteractiveShellCommand {
try {
txIdHashParsed = SecureHash.parse(txIdHash);
} catch (IllegalArgumentException e) {
out.println("The provided string is not a valid hexadecimal SHA-256 hash value", Color.red);
out.println("The provided string is not a valid hexadecimal SHA-256 hash value", Decoration.bold, Color.red);
return;
}
@ -53,7 +54,7 @@ public class HashLookupShellCommand extends InteractiveShellCommand {
SecureHash found = match.get();
out.println("Found a matching transaction with Id: " + found.toString());
} else {
out.println("No matching transaction found", Color.red);
out.println("No matching transaction found", Decoration.bold, Color.red);
}
}
}

View File

@ -38,6 +38,7 @@ import net.corda.core.messaging.pendingFlowsCount
import net.corda.tools.shell.utlities.ANSIProgressRenderer
import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer
import org.crsh.command.InvocationContext
import org.crsh.command.ShellSafety
import org.crsh.console.jline.JLineProcessor
import org.crsh.console.jline.TerminalFactory
import org.crsh.console.jline.console.ConsoleReader
@ -50,6 +51,7 @@ import org.crsh.shell.Shell
import org.crsh.shell.ShellFactory
import org.crsh.shell.impl.command.ExternalResolver
import org.crsh.text.Color
import org.crsh.text.Decoration
import org.crsh.text.RenderPrintWriter
import org.crsh.util.InterruptHandler
import org.crsh.util.Utils
@ -87,6 +89,8 @@ import kotlin.concurrent.thread
// TODO: Resurrect or reimplement the mail plugin.
// TODO: Make it notice new shell commands added after the node started.
const val STANDALONE_SHELL_PERMISSION = "ALL"
@Suppress("MaxLineLength")
object InteractiveShell {
private val log = LoggerFactory.getLogger(javaClass)
@ -128,14 +132,21 @@ object InteractiveShell {
rpcConn = connection
connection.proxy as InternalCordaRPCOps
}
_startShell(configuration, classLoader)
launchShell(configuration, standalone, classLoader)
}
private fun _startShell(configuration: ShellConfiguration, classLoader: ClassLoader? = null) {
private fun launchShell(configuration: ShellConfiguration, standalone: Boolean, classLoader: ClassLoader? = null) {
shellConfiguration = configuration
InteractiveShell.classLoader = classLoader
val runSshDaemon = configuration.sshdPort != null
var runShellInSafeMode = true
if (!standalone) {
log.info("launchShell: User=${configuration.user} perm=${configuration.permissions}")
log.info("Shell: PermitExit= ${configuration.localShellAllowExitInSafeMode}, UNSAFELOCAL=${configuration.localShellUnsafe}")
runShellInSafeMode = configuration.permissions?.filter { it.contains(STANDALONE_SHELL_PERMISSION); }?.isEmpty() != false
}
val config = Properties()
if (runSshDaemon) {
// Enable SSH access. Note: these have to be strings, even though raw object assignments also work.
@ -183,7 +194,14 @@ object InteractiveShell {
"Commands to extract information about checkpoints stored within the node",
CheckpointShellCommand::class.java
)
shell = ShellLifecycle(configuration.commandsDirectory).start(config, configuration.user, configuration.password)
val shellSafety = ShellSafety().apply {
setSafeShell(runShellInSafeMode)
setInternal(!standalone)
setStandAlone(standalone)
setAllowExitInSafeMode(configuration.localShellAllowExitInSafeMode || standalone)
}
shell = ShellLifecycle(configuration.commandsDirectory, shellSafety).start(config, configuration.user, configuration.password)
}
fun runLocalShell(onExit: () -> Unit = {}) {
@ -210,7 +228,7 @@ object InteractiveShell {
}
}
class ShellLifecycle(private val shellCommands: Path) : PluginLifeCycle() {
class ShellLifecycle(private val shellCommands: Path, private val shellSafety: ShellSafety) : PluginLifeCycle() {
fun start(config: Properties, localUserName: String = "", localUserPassword: String = ""): Shell {
val classLoader = this.javaClass.classLoader
val classpathDriver = ClassPathMountFactory(classLoader)
@ -243,7 +261,8 @@ object InteractiveShell {
this.config = config
start(context)
ops = makeRPCOps(rpcOps, localUserName, localUserPassword)
return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, ops, StdoutANSIProgressRenderer))
return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, ops,
StdoutANSIProgressRenderer), shellSafety)
}
}
@ -317,15 +336,15 @@ object InteractiveShell {
val matches = try {
rpcOps.registeredFlows().filter { nameFragment in it }
} catch (e: PermissionException) {
output.println(e.message ?: "Access denied", Color.red)
output.println(e.message ?: "Access denied", Decoration.bold, Color.red)
return
}
if (matches.isEmpty()) {
output.println("No matching flow found, run 'flow list' to see your options.", Color.red)
output.println("No matching flow found, run 'flow list' to see your options.", Decoration.bold, Color.red)
return
} else if (matches.size > 1 && matches.find { it.endsWith(nameFragment)} == null) {
output.println("Ambiguous name provided, please be more specific. Your options are:")
matches.forEachIndexed { i, s -> output.println("${i + 1}. $s", Color.yellow) }
matches.forEachIndexed { i, s -> output.println("${i + 1}. $s", Decoration.bold, Color.yellow) }
return
}
@ -364,10 +383,10 @@ object InteractiveShell {
}
output.println("Flow completed with result: ${stateObservable.returnValue.get()}")
} catch (e: NoApplicableConstructor) {
output.println("No matching constructor found:", Color.red)
e.errors.forEach { output.println("- $it", Color.red) }
output.println("No matching constructor found:", Decoration.bold, Color.red)
e.errors.forEach { output.println("- $it", Decoration.bold, Color.red) }
} catch (e: PermissionException) {
output.println(e.message ?: "Access denied", Color.red)
output.println(e.message ?: "Access denied", Decoration.bold, Color.red)
} catch (e: ExecutionException) {
// ignoring it as already logged by the progress handler subscriber
} finally {
@ -421,7 +440,7 @@ object InteractiveShell {
val runId = try {
inputObjectMapper.readValue(id, StateMachineRunId::class.java)
} catch (e: JsonMappingException) {
output.println("Cannot parse flow ID of '$id' - expecting a UUID.", Color.red)
output.println("Cannot parse flow ID of '$id' - expecting a UUID.", Decoration.bold, Color.red)
log.error("Failed to parse flow ID", e)
return
}
@ -429,14 +448,14 @@ object InteractiveShell {
if (id.length < uuidStringSize) {
val msg = "Can not kill the flow. Flow ID of '$id' seems to be malformed - a UUID should have $uuidStringSize characters. " +
"Expand the terminal window to see the full UUID value."
output.println(msg, Color.red)
output.println(msg, Decoration.bold, Color.red)
log.warn(msg)
return
}
if (rpcOps.killFlow(runId)) {
output.println("Killed flow $runId", Color.yellow)
output.println("Killed flow $runId", Decoration.bold, Color.yellow)
} else {
output.println("Failed to kill flow $runId", Color.red)
output.println("Failed to kill flow $runId", Decoration.bold, Color.red)
}
} finally {
output.flush()
@ -568,7 +587,7 @@ object InteractiveShell {
// The flow command provides better support and startFlow requires special handling anyway due to
// the generic startFlow RPC interface which offers no type information with which to parse the
// string form of the command.
out.println("Please use the 'flow' command to interact with flows rather than the 'run' command.", Color.yellow)
out.println("Please use the 'flow' command to interact with flows rather than the 'run' command.", Decoration.bold, Color.yellow)
return null
} else if (cmd.substringAfter(" ").trim().equals("gracefulShutdown", ignoreCase = true)) {
return gracefulShutdown(out, cordaRPCOps)
@ -603,12 +622,12 @@ object InteractiveShell {
}
}
} catch (e: StringToMethodCallParser.UnparseableCallException) {
out.println(e.message, Color.red)
out.println(e.message, Decoration.bold, Color.red)
if (e !is StringToMethodCallParser.UnparseableCallException.NoSuchFile) {
out.println("Please try 'man run' to learn what syntax is acceptable")
}
} catch (e: Exception) {
out.println("RPC failed: ${e.rootCause}", Color.red)
out.println("RPC failed: ${e.rootCause}", Decoration.bold, Color.red)
} finally {
InputStreamSerializer.invokeContext = null
InputStreamDeserializer.closeAll()

View File

@ -9,13 +9,15 @@ data class ShellConfiguration(
val cordappsDirectory: Path? = null,
var user: String = "",
var password: String = "",
var permissions: Set<String>? = null,
var localShellAllowExitInSafeMode: Boolean = false,
var localShellUnsafe: Boolean = false,
val hostAndPort: NetworkHostAndPort,
val ssl: ClientRpcSslOptions? = null,
val sshdPort: Int? = null,
val sshHostKeyDirectory: Path? = null,
val noLocalShell: Boolean = false) {
companion object {
const val SSH_PORT = 2222
const val COMMANDS_DIR = "shell-commands"
const val CORDAPPS_DIR = "cordapps"
const val SSHD_HOSTKEY_DIR = "ssh"

View File

@ -31,6 +31,7 @@ import net.corda.testing.core.getTestPartyAndCertificate
import net.corda.testing.internal.DEV_ROOT_CA
import org.crsh.command.InvocationContext
import org.crsh.text.Color
import org.crsh.text.Decoration
import org.crsh.text.RenderPrintWriter
import org.junit.Before
import org.junit.Test
@ -235,7 +236,7 @@ class InteractiveShellTest {
@Test
fun killFlowWithNonsenseID() {
InteractiveShell.killFlowById("nonsense", printWriter, cordaRpcOps, om)
verify(printWriter).println("Cannot parse flow ID of 'nonsense' - expecting a UUID.", Color.red)
verify(printWriter).println("Cannot parse flow ID of 'nonsense' - expecting a UUID.", Decoration.bold, Color.red)
verify(printWriter).flush()
}
@ -246,7 +247,7 @@ class InteractiveShellTest {
InteractiveShell.killFlowById(runId.uuid.toString(), printWriter, cordaRpcOps, om)
verify(cordaRpcOps).killFlow(runId)
verify(printWriter).println("Failed to kill flow $runId", Color.red)
verify(printWriter).println("Failed to kill flow $runId", Decoration.bold, Color.red)
verify(printWriter).flush()
}
@ -257,7 +258,7 @@ class InteractiveShellTest {
InteractiveShell.killFlowById(runId.uuid.toString(), printWriter, cordaRpcOps, om)
verify(cordaRpcOps).killFlow(runId)
verify(printWriter).println("Killed flow $runId", Color.yellow)
verify(printWriter).println("Killed flow $runId", Decoration.bold, Color.yellow)
verify(printWriter).flush()
}
}