/* * JBoss, Home of Professional Open Source. * Copyright 2016, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.cli.impl; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.ServiceLoader; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.Configuration; import javax.security.sasl.RealmCallback; import javax.security.sasl.RealmChoiceCallback; import javax.security.sasl.SaslException; import org.jboss.aesh.console.AeshConsoleCallback; import org.jboss.aesh.console.ConsoleCallback; import org.jboss.aesh.console.ConsoleOperation; import org.jboss.aesh.console.Process; import org.jboss.aesh.console.helper.InterruptHook; import org.jboss.aesh.console.settings.FileAccessPermission; import org.jboss.aesh.console.settings.Settings; import org.jboss.aesh.console.settings.SettingsBuilder; import org.jboss.aesh.edit.actions.Action; import org.jboss.as.cli.Attachments; import org.jboss.as.cli.CliConfig; import org.jboss.as.cli.CliEvent; import org.jboss.as.cli.CliEventListener; import org.jboss.as.cli.CliInitializationException; import org.jboss.as.cli.CommandCompleter; import org.jboss.as.cli.CommandContext; import org.jboss.as.cli.CommandFormatException; import org.jboss.as.cli.CommandHandler; import org.jboss.as.cli.CommandHandlerProvider; import org.jboss.as.cli.CommandHistory; import org.jboss.as.cli.CommandLineCompleter; import org.jboss.as.cli.CommandLineException; import org.jboss.as.cli.CommandLineRedirection; import org.jboss.as.cli.CommandRegistry; import org.jboss.as.cli.ConnectionInfo; import org.jboss.as.cli.ControllerAddress; import org.jboss.as.cli.ControllerAddressResolver; import org.jboss.as.cli.OperationCommand; import org.jboss.as.cli.OperationCommand.HandledRequest; import org.jboss.as.cli.RequestWithAttachments; import org.jboss.as.cli.SSLConfig; import org.jboss.as.cli.Util; import org.jboss.as.cli.batch.Batch; import org.jboss.as.cli.batch.BatchManager; import org.jboss.as.cli.batch.BatchedCommand; import org.jboss.as.cli.batch.impl.DefaultBatchManager; import org.jboss.as.cli.batch.impl.DefaultBatchedCommand; import org.jboss.as.cli.embedded.EmbeddedControllerHandlerRegistrar; import org.jboss.as.cli.embedded.EmbeddedProcessLaunch; import org.jboss.as.cli.handlers.ArchiveHandler; import org.jboss.as.cli.handlers.ClearScreenHandler; import org.jboss.as.cli.handlers.CommandCommandHandler; import org.jboss.as.cli.handlers.ConnectHandler; import org.jboss.as.cli.handlers.ConnectionInfoHandler; import org.jboss.as.cli.handlers.DeployHandler; import org.jboss.as.cli.handlers.DeploymentInfoHandler; import org.jboss.as.cli.handlers.DeploymentOverlayHandler; import org.jboss.as.cli.handlers.EchoDMRHandler; import org.jboss.as.cli.handlers.EchoVariableHandler; import org.jboss.as.cli.handlers.GenericTypeOperationHandler; import org.jboss.as.cli.handlers.HelpHandler; import org.jboss.as.cli.handlers.HistoryHandler; import org.jboss.as.cli.handlers.LsHandler; import org.jboss.as.cli.handlers.OperationRequestHandler; import org.jboss.as.cli.handlers.PrefixHandler; import org.jboss.as.cli.handlers.PrintWorkingNodeHandler; import org.jboss.as.cli.handlers.QuitHandler; import org.jboss.as.cli.handlers.ReadAttributeHandler; import org.jboss.as.cli.handlers.ReadOperationHandler; import org.jboss.as.cli.handlers.ReloadHandler; import org.jboss.as.cli.handlers.SetVariableHandler; import org.jboss.as.cli.handlers.ShutdownHandler; import org.jboss.as.cli.handlers.CommandTimeoutHandler; import org.jboss.as.cli.handlers.AttachmentHandler; import org.jboss.as.cli.handlers.UndeployHandler; import org.jboss.as.cli.handlers.UnsetVariableHandler; import org.jboss.as.cli.handlers.VersionHandler; import org.jboss.as.cli.handlers.batch.BatchClearHandler; import org.jboss.as.cli.handlers.batch.BatchDiscardHandler; import org.jboss.as.cli.handlers.batch.BatchEditLineHandler; import org.jboss.as.cli.handlers.batch.BatchHandler; import org.jboss.as.cli.handlers.batch.BatchHoldbackHandler; import org.jboss.as.cli.handlers.batch.BatchListHandler; import org.jboss.as.cli.handlers.batch.BatchMoveLineHandler; import org.jboss.as.cli.handlers.batch.BatchRemoveLineHandler; import org.jboss.as.cli.handlers.batch.BatchRunHandler; import org.jboss.as.cli.handlers.ifelse.ElseHandler; import org.jboss.as.cli.handlers.ifelse.EndIfHandler; import org.jboss.as.cli.handlers.ifelse.IfHandler; import org.jboss.as.cli.handlers.jca.DataSourceAddCompositeHandler; import org.jboss.as.cli.handlers.jca.JDBCDriverInfoHandler; import org.jboss.as.cli.handlers.jca.JDBCDriverNameProvider; import org.jboss.as.cli.handlers.jca.XADataSourceAddCompositeHandler; import org.jboss.as.cli.handlers.module.ASModuleHandler; import org.jboss.as.cli.handlers.trycatch.CatchHandler; import org.jboss.as.cli.handlers.trycatch.EndTryHandler; import org.jboss.as.cli.handlers.trycatch.FinallyHandler; import org.jboss.as.cli.handlers.trycatch.TryHandler; import org.jboss.as.cli.operation.CommandLineParser; import org.jboss.as.cli.operation.NodePathFormatter; import org.jboss.as.cli.operation.OperationCandidatesProvider; import org.jboss.as.cli.operation.OperationFormatException; import org.jboss.as.cli.operation.OperationRequestAddress; import org.jboss.as.cli.operation.ParsedCommandLine; import org.jboss.as.cli.operation.impl.DefaultCallbackHandler; import org.jboss.as.cli.operation.impl.DefaultOperationCandidatesProvider; import org.jboss.as.cli.operation.impl.DefaultOperationRequestAddress; import org.jboss.as.cli.operation.impl.DefaultOperationRequestBuilder; import org.jboss.as.cli.operation.impl.DefaultOperationRequestParser; import org.jboss.as.cli.operation.impl.DefaultPrefixFormatter; import org.jboss.as.cli.operation.impl.RolloutPlanCompleter; import org.jboss.as.cli.parsing.command.CommandFormat; import org.jboss.as.cli.parsing.operation.OperationFormat; import org.jboss.as.cli.util.FingerprintGenerator; import org.jboss.as.controller.client.ModelControllerClient; import org.jboss.as.controller.client.Operation; import org.jboss.as.controller.client.OperationBuilder; import org.jboss.as.protocol.GeneralTimeoutHandler; import org.jboss.as.protocol.StreamUtils; import org.jboss.dmr.ModelNode; import org.jboss.logging.Logger; import org.jboss.logging.Logger.Level; import org.jboss.stdio.StdioContext; import org.wildfly.security.auth.callback.CredentialCallback; import org.wildfly.security.credential.PasswordCredential; import org.wildfly.security.manager.WildFlySecurityManager; import org.wildfly.security.password.interfaces.ClearPassword; import org.wildfly.security.password.interfaces.DigestPassword; import org.wildfly.security.util.CodePointIterator; import org.xnio.http.RedirectException; /** * * @author Alexey Loubyansky */ class CommandContextImpl implements CommandContext, ModelControllerClientFactory.ConnectionCloseHandler { private static final Logger log = Logger.getLogger(CommandContext.class); private static final byte RUNNING = 0; private static final byte TERMINATING = 1; private static final byte TERMINATED = 2; /** * State Tracking * * Interact - Interactive UI * * Silent - Only send input. No output. * Error On Interact - If non-interactive mode requests user interaction, throw an error. */ private boolean INTERACT = false; private boolean SILENT = false; private boolean ERROR_ON_INTERACT = false; /** the cli configuration */ private final CliConfig config; private final ControllerAddressResolver addressResolver; /** command registry */ private final CommandRegistry cmdRegistry = new CommandRegistry(); /** loads command handlers from the domain management model extensions */ private ExtensionsLoader extLoader; private Console console; /** whether the session should be terminated */ private byte terminate; /** current command line */ private String cmdLine; /** parsed command arguments */ private DefaultCallbackHandler parsedCmd = new DefaultCallbackHandler(true); /** domain or standalone mode */ private boolean domainMode; /** the controller client */ private ModelControllerClient client; /** the address of the current controller */ private ControllerAddress currentAddress; /** the command line specified username */ private final String username; /** the command line specified password */ private final char[] password; /** flag to disable the local authentication mechanism */ private final boolean disableLocalAuth; /** The SSLContext when managed by the CLI */ private SSLContext sslContext; /** The TrustManager in use by the SSLContext, a reference is kept to rejected certificates can be captured. */ private LazyDelagatingTrustManager trustManager; /** various key/value pairs */ private Map<Scope, Map<String, Object>> map = new HashMap<>(); /** operation request address prefix */ private final OperationRequestAddress prefix = new DefaultOperationRequestAddress(); /** the prefix formatter */ private final NodePathFormatter prefixFormatter = DefaultPrefixFormatter.INSTANCE; /** provider of operation request candidates for tab-completion */ private final OperationCandidatesProvider operationCandidatesProvider; /** operation request handler */ private final OperationRequestHandler operationHandler; /** batches */ private BatchManager batchManager = new DefaultBatchManager(); /** the default command completer */ private final CommandCompleter cmdCompleter; /** the timeout handler */ private final GeneralTimeoutHandler timeoutHandler = new GeneralTimeoutHandler(); /** the client bind address */ private final String clientBindAddress; /** output target */ private BufferedWriter outputTarget; private List<CliEventListener> listeners = new ArrayList<CliEventListener>(); /** the value of this variable will be used as the exit code of the vm, it is reset by every command/operation executed */ private int exitCode; private File currentDir = new File(""); /** whether to resolve system properties passed in as values of operation parameters*/ private boolean resolveParameterValues; private Map<String, String> variables; private CliShutdownHook.Handler shutdownHook; /** command line handling redirection */ private CommandLineRedirectionRegistration redirection; /** this object saves information to be used in ConnectionInfoHandler */ private ConnectionInfoBean connInfoBean; private final CLIPrintStream cliPrintStream; // Store a ref to the default input stream aesh will use before we do any manipulation of stdin //private InputStream stdIn = new SettingsBuilder().create().getInputStream(); private boolean uninstallIO; private static JaasConfigurationWrapper jaasConfigurationWrapper; // we want this wrapper to be only created once private final boolean echoCommand; private static final short DEFAULT_TIMEOUT = 0; private int timeout = DEFAULT_TIMEOUT; private int configTimeout; /** * Version mode - only used when --version is called from the command line. * * @throws CliInitializationException */ CommandContextImpl() throws CliInitializationException { this.console = null; this.operationCandidatesProvider = null; this.cmdCompleter = null; operationHandler = new OperationRequestHandler(); initStdIO(); try { initCommands(); } catch (CommandLineException e) { throw new CliInitializationException("Failed to initialize commands", e); } config = CliConfigImpl.load(this); addressResolver = ControllerAddressResolver.newInstance(config, null); resolveParameterValues = config.isResolveParameterValues(); SILENT = config.isSilent(); ERROR_ON_INTERACT = config.isErrorOnInteract(); echoCommand = config.isEchoCommand(); configTimeout = config.getCommandTimeout() == null ? DEFAULT_TIMEOUT : config.getCommandTimeout(); setCommandTimeout(configTimeout); username = null; password = null; disableLocalAuth = false; clientBindAddress = null; cliPrintStream = new CLIPrintStream(); initSSLContext(); initJaasConfig(); addShutdownHook(); CliLauncher.runcom(this); } /** * Default constructor used for both interactive and non-interactive mode. * */ CommandContextImpl(CommandContextConfiguration configuration) throws CliInitializationException { config = CliConfigImpl.load(this, configuration); addressResolver = ControllerAddressResolver.newInstance(config, configuration.getController()); operationHandler = new OperationRequestHandler(); this.username = configuration.getUsername(); this.password = configuration.getPassword(); this.disableLocalAuth = configuration.isDisableLocalAuth(); this.clientBindAddress = configuration.getClientBindAddress(); SILENT = config.isSilent(); ERROR_ON_INTERACT = config.isErrorOnInteract(); echoCommand = config.isEchoCommand(); configTimeout = config.getCommandTimeout() == null ? DEFAULT_TIMEOUT : config.getCommandTimeout(); setCommandTimeout(configTimeout); resolveParameterValues = config.isResolveParameterValues(); cliPrintStream = configuration.getConsoleOutput() == null ? new CLIPrintStream() : new CLIPrintStream(configuration.getConsoleOutput()); initStdIO(); try { initCommands(); } catch (CommandLineException e) { throw new CliInitializationException("Failed to initialize commands", e); } initSSLContext(); initJaasConfig(); if (configuration.isInitConsole() || configuration.getConsoleInput() != null) { cmdCompleter = new CommandCompleter(cmdRegistry); // we don't ask to start the console here because it will start reading the input immediately // this will break in case the launching line had input redirection and switch to connect to the controller, // e.g. jboss-cli.ch -c < some_file // the input will be read before the connection is established initBasicConsole(configuration.getConsoleInput(), false); console.addCompleter(cmdCompleter); this.operationCandidatesProvider = new DefaultOperationCandidatesProvider(); } else { this.cmdCompleter = null; this.operationCandidatesProvider = null; } addShutdownHook(); CliLauncher.runcom(this); } protected void addShutdownHook() { shutdownHook = new CliShutdownHook.Handler() { @Override public void shutdown() { if (CommandContextImpl.this.console != null) { CommandContextImpl.this.console.interrupt(); } terminateSession(); }}; CliShutdownHook.add(shutdownHook); } protected void initBasicConsole(InputStream consoleInput) throws CliInitializationException { initBasicConsole(consoleInput, true); } protected void initBasicConsole(InputStream consoleInput, boolean start) throws CliInitializationException { // this method shouldn't be called twice during the session assert console == null : "the console has already been initialized"; Settings settings = createSettings(consoleInput); this.console = Console.Factory.getConsole(this, settings); console.setCallback(initCallback()); if(start) { console.start(); } } private ConsoleCallback initCallback() { return new CLIAeshConsoleCallback() { @Override public int execute(ConsoleOperation output) throws InterruptedException { // Track the active process setActiveProcess(true); try { // Actual work stuff if (cmdCompleter == null) { throw new IllegalStateException("The console hasn't been initialized at construction time."); } if (output.getBuffer() == null) terminateSession(); else { handleSafe(output.getBuffer().trim()); if (INTERACT && terminate == RUNNING) { console.setPrompt(getPrompt()); } } return 0; }finally{ setActiveProcess(false); } } }; } abstract class CLIAeshConsoleCallback extends AeshConsoleCallback{ private Process process; public boolean hasActiveProcess() { return activeProcess; } public void setActiveProcess(boolean activeProcess) { this.activeProcess = activeProcess; } private boolean activeProcess = false; @Override public void setProcess(org.jboss.aesh.console.Process process){ this.process = process; } public int getProcessPID(){ return process.getPID(); } } private Settings createSettings(InputStream consoleInput) { SettingsBuilder settings = new SettingsBuilder(); if(consoleInput != null) { settings.inputStream(consoleInput); } settings.outputStream(cliPrintStream); settings.enableExport(false); settings.disableHistory(!config.isHistoryEnabled()); settings.historyFile(new File(config.getHistoryFileDir(), config.getHistoryFileName())); settings.historySize(config.getHistoryMaxSize()); // Modify Default History File Permissions FileAccessPermission permissions = new FileAccessPermission(); permissions.setReadableOwnerOnly(true); permissions.setWritableOwnerOnly(true); settings.historyFilePermission(permissions); settings.parseOperators(false); settings.interruptHook( new InterruptHook() { @Override public void handleInterrupt(org.jboss.aesh.console.Console console, Action action) { if(shutdownHook != null ){ shutdownHook.shutdown(); }else { terminateSession(); } Thread.currentThread().interrupt(); } } ); return settings.create(); } private void initStdIO() { try { StdioContext.install(); this.uninstallIO = true; } catch (IllegalStateException e) { this.uninstallIO = false; } } private void restoreStdIO() { if (uninstallIO) { try { StdioContext.uninstall(); } catch (IllegalStateException ignored) { // someone else must have uninstalled } } } private void initCommands() throws CommandLineException { cmdRegistry.registerHandler(new AttachmentHandler(this), "attachment"); cmdRegistry.registerHandler(new PrefixHandler(), "cd", "cn"); cmdRegistry.registerHandler(new ClearScreenHandler(), "clear", "cls"); cmdRegistry.registerHandler(new CommandCommandHandler(cmdRegistry), "command"); cmdRegistry.registerHandler(new ConnectHandler(), "connect"); cmdRegistry.registerHandler(new EchoDMRHandler(), "echo-dmr"); cmdRegistry.registerHandler(new HelpHandler(cmdRegistry), "help", "h"); cmdRegistry.registerHandler(new HistoryHandler(), "history"); cmdRegistry.registerHandler(new LsHandler(this), "ls"); cmdRegistry.registerHandler(new ASModuleHandler(this), "module"); cmdRegistry.registerHandler(new PrintWorkingNodeHandler(), "pwd", "pwn"); cmdRegistry.registerHandler(new QuitHandler(), "quit", "q", "exit"); cmdRegistry.registerHandler(new ReadAttributeHandler(this), "read-attribute"); cmdRegistry.registerHandler(new ReadOperationHandler(this), "read-operation"); cmdRegistry.registerHandler(new VersionHandler(), "version"); cmdRegistry.registerHandler(new ConnectionInfoHandler(), "connection-info"); // command-timeout cmdRegistry.registerHandler(new CommandTimeoutHandler(), "command-timeout"); // variables cmdRegistry.registerHandler(new SetVariableHandler(), "set"); cmdRegistry.registerHandler(new EchoVariableHandler(), "echo"); cmdRegistry.registerHandler(new UnsetVariableHandler(), "unset"); // deployment cmdRegistry.registerHandler(new DeployHandler(this), "deploy"); cmdRegistry.registerHandler(new UndeployHandler(this), "undeploy"); cmdRegistry.registerHandler(new DeploymentInfoHandler(this), "deployment-info"); cmdRegistry.registerHandler(new DeploymentOverlayHandler(this), "deployment-overlay"); // batch commands cmdRegistry.registerHandler(new BatchHandler(this), "batch"); cmdRegistry.registerHandler(new BatchDiscardHandler(), "discard-batch"); cmdRegistry.registerHandler(new BatchListHandler(), "list-batch"); cmdRegistry.registerHandler(new BatchHoldbackHandler(), "holdback-batch"); cmdRegistry.registerHandler(new BatchRunHandler(this), "run-batch"); cmdRegistry.registerHandler(new BatchClearHandler(), "clear-batch"); cmdRegistry.registerHandler(new BatchRemoveLineHandler(), "remove-batch-line"); cmdRegistry.registerHandler(new BatchMoveLineHandler(), "move-batch-line"); cmdRegistry.registerHandler(new BatchEditLineHandler(), "edit-batch-line"); // try-catch cmdRegistry.registerHandler(new TryHandler(), "try"); cmdRegistry.registerHandler(new CatchHandler(), "catch"); cmdRegistry.registerHandler(new FinallyHandler(), "finally"); cmdRegistry.registerHandler(new EndTryHandler(), "end-try"); // if else cmdRegistry.registerHandler(new IfHandler(), "if"); cmdRegistry.registerHandler(new ElseHandler(), "else"); cmdRegistry.registerHandler(new EndIfHandler(), "end-if"); // data-source final DefaultCompleter driverNameCompleter = new DefaultCompleter(JDBCDriverNameProvider.INSTANCE); final GenericTypeOperationHandler dsHandler = new GenericTypeOperationHandler(this, "/subsystem=datasources/data-source", null); dsHandler.addValueCompleter(Util.DRIVER_NAME, driverNameCompleter); // override the add operation with the handler that accepts connection props final DataSourceAddCompositeHandler dsAddHandler = new DataSourceAddCompositeHandler(this, "/subsystem=datasources/data-source"); dsAddHandler.addValueCompleter(Util.DRIVER_NAME, driverNameCompleter); dsHandler.addHandler(Util.ADD, dsAddHandler); cmdRegistry.registerHandler(dsHandler, "data-source"); final GenericTypeOperationHandler xaDsHandler = new GenericTypeOperationHandler(this, "/subsystem=datasources/xa-data-source", null); xaDsHandler.addValueCompleter(Util.DRIVER_NAME, driverNameCompleter); // override the xa add operation with the handler that accepts xa props final XADataSourceAddCompositeHandler xaDsAddHandler = new XADataSourceAddCompositeHandler(this, "/subsystem=datasources/xa-data-source"); xaDsAddHandler.addValueCompleter(Util.DRIVER_NAME, driverNameCompleter); xaDsHandler.addHandler(Util.ADD, xaDsAddHandler); cmdRegistry.registerHandler(xaDsHandler, "xa-data-source"); cmdRegistry.registerHandler(new JDBCDriverInfoHandler(this), "jdbc-driver-info"); // rollout plan final GenericTypeOperationHandler rolloutPlan = new GenericTypeOperationHandler(this, "/management-client-content=rollout-plans/rollout-plan", null); rolloutPlan.addValueConverter("content", HeadersArgumentValueConverter.INSTANCE); rolloutPlan.addValueCompleter("content", RolloutPlanCompleter.INSTANCE); cmdRegistry.registerHandler(rolloutPlan, "rollout-plan"); // supported but hidden from tab-completion until stable implementation cmdRegistry.registerHandler(new ArchiveHandler(this), false, "archive"); final AtomicReference<EmbeddedProcessLaunch> embeddedServerLaunch = EmbeddedControllerHandlerRegistrar.registerEmbeddedCommands(cmdRegistry, this); cmdRegistry.registerHandler(new ReloadHandler(this, embeddedServerLaunch), "reload"); cmdRegistry.registerHandler(new ShutdownHandler(this, embeddedServerLaunch), "shutdown"); registerExtraHandlers(); extLoader = new ExtensionsLoader(cmdRegistry, this); } private void registerExtraHandlers() throws CommandLineException { ServiceLoader<CommandHandlerProvider> loader = ServiceLoader.load(CommandHandlerProvider.class); for (CommandHandlerProvider provider : loader) { cmdRegistry.registerHandler(provider.createCommandHandler(this), provider.isTabComplete(), provider.getNames()); } } public int getExitCode() { return exitCode; } /** * Initialise the SSLContext and associated TrustManager for this CommandContext. * * If no configuration is specified the default mode of operation will be to use a lazily initialised TrustManager with no * KeyManager. */ private void initSSLContext() throws CliInitializationException { // If the standard properties have been set don't enable and CLI specific stores. if (WildFlySecurityManager.getPropertyPrivileged("javax.net.ssl.keyStore", null) != null || WildFlySecurityManager.getPropertyPrivileged("javax.net.ssl.trustStore", null) != null) { return; } KeyManager[] keyManagers = null; TrustManager[] trustManagers = null; String trustStore = null; String trustStorePassword = null; boolean modifyTrustStore = true; SSLConfig sslConfig = config.getSslConfig(); if (sslConfig != null) { String keyStoreLoc = sslConfig.getKeyStore(); if (keyStoreLoc != null) { char[] keyStorePassword = sslConfig.getKeyStorePassword().toCharArray(); String tmpKeyPassword = sslConfig.getKeyPassword(); char[] keyPassword = tmpKeyPassword != null ? tmpKeyPassword.toCharArray() : keyStorePassword; File keyStoreFile = new File(keyStoreLoc); FileInputStream fis = null; try { fis = new FileInputStream(keyStoreFile); KeyStore theKeyStore = KeyStore.getInstance("JKS"); theKeyStore.load(fis, keyStorePassword); String alias = sslConfig.getAlias(); if (alias != null) { KeyStore replacement = KeyStore.getInstance("JKS"); replacement.load(null); KeyStore.ProtectionParameter protection = new KeyStore.PasswordProtection(keyPassword); replacement.setEntry(alias, theKeyStore.getEntry(alias, protection), protection); theKeyStore = replacement; } KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(theKeyStore, keyPassword); keyManagers = keyManagerFactory.getKeyManagers(); } catch (IOException e) { throw new CliInitializationException(e); } catch (GeneralSecurityException e) { throw new CliInitializationException(e); } finally { StreamUtils.safeClose(fis); } } trustStore = sslConfig.getTrustStore(); trustStorePassword = sslConfig.getTrustStorePassword(); modifyTrustStore = sslConfig.isModifyTrustStore(); } if (trustStore == null) { final String userHome = WildFlySecurityManager.getPropertyPrivileged("user.home", null); File trustStoreFile = new File(userHome, ".jboss-cli.truststore"); trustStore = trustStoreFile.getAbsolutePath(); trustStorePassword = "cli_truststore"; // Risk of modification but no private keys to be stored in the truststore. } trustManager = new LazyDelagatingTrustManager(trustStore, trustStorePassword, modifyTrustStore); trustManagers = new TrustManager[] { trustManager }; try { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagers, trustManagers, null); this.sslContext = sslContext; } catch (GeneralSecurityException e) { throw new CliInitializationException(e); } } /** * The underlying SASL mechanisms may require a JAAS definition, unless a more specific definition as been provided use our * own definition for GSSAPI. */ private void initJaasConfig() { // create the wrapper only once to avoid memory leak if (jaasConfigurationWrapper == null) { Configuration coreConfig = null; try { coreConfig = SecurityActions.getGlobalJaasConfiguration(); } catch (SecurityException e) { log.debug("Unable to obtain default configuration", e); } jaasConfigurationWrapper = new JaasConfigurationWrapper(coreConfig); SecurityActions.setGlobalJaasConfiguration(jaasConfigurationWrapper); } } @Override public boolean isTerminated() { return terminate == TERMINATED; } private StringBuilder lineBuffer; private StringBuilder origLineBuffer; private CommandExecutor executor = new CommandExecutor(this); @Override public void handle(String line) throws CommandLineException { if (line.isEmpty() || line.charAt(0) == '#') { return; // ignore comments } int i = line.length() - 1; while(i > 0 && line.charAt(i) <= ' ') { if(line.charAt(--i) == '\\') { break; } } String echoLine = line; if(line.charAt(i) == '\\') { if(lineBuffer == null) { lineBuffer = new StringBuilder(); origLineBuffer = new StringBuilder(); } lineBuffer.append(line, 0, i); lineBuffer.append(' '); origLineBuffer.append(line, 0, i); origLineBuffer.append('\n'); return; } else if(lineBuffer != null) { lineBuffer.append(line); origLineBuffer.append(line); echoLine = origLineBuffer.toString(); line = lineBuffer.toString(); lineBuffer = null; } if (echoCommand && !INTERACT && redirection == null) { printLine(getPrompt() + echoLine); } resetArgs(line); try { if(redirection != null) { redirection.target.handle(this); } else if (parsedCmd.getFormat() == OperationFormat.INSTANCE) { if (isBatchMode()) { Batch batch = getBatchManager().getActiveBatch(); final ModelNode request = Util.toOperationRequest(this, parsedCmd, batch.getAttachments()); StringBuilder op = new StringBuilder(); op.append(getNodePathFormatter().format(parsedCmd.getAddress())); op.append(line.substring(line.indexOf(':'))); DefaultBatchedCommand batchedCmd = new DefaultBatchedCommand(this, op.toString(), request, null); batch.add(batchedCmd); } else { Attachments attachments = new Attachments(); final ModelNode op = Util.toOperationRequest(this, parsedCmd, attachments); RequestWithAttachments req = new RequestWithAttachments(op, attachments); set(Scope.REQUEST, "OP_REQ", req); operationHandler.handle(this); } } else { final String cmdName = parsedCmd.getOperationName(); CommandHandler handler = cmdRegistry.getCommandHandler(cmdName.toLowerCase()); if (handler != null) { if (isBatchMode() && handler.isBatchMode(this)) { if (!(handler instanceof OperationCommand)) { throw new CommandLineException("The command is not allowed in a batch."); } else { try { Batch batch = getBatchManager().getActiveBatch(); HandledRequest request = ((OperationCommand) handler).buildHandledRequest(this, batch.getAttachments()); BatchedCommand batchedCmd = new DefaultBatchedCommand(this, line, request.getRequest(), request.getResponseHandler()); batch.add(batchedCmd); } catch (CommandFormatException e) { throw new CommandFormatException("Failed to add to batch '" + line + "'", e); } } } else { execute(() -> { executor.execute(handler, timeout, TimeUnit.SECONDS); return null; }, line); } } else { throw new CommandLineException("Unexpected command '" + line + "'. Type 'help --commands' for the list of supported commands."); } } } catch(CommandLineException e) { throw e; } catch (Throwable t) { if(log.isDebugEnabled()) { log.debug("Failed to handle '" + line + "'", t); } throw new CommandLineException("Failed to handle '" + line + "'", t); } finally { // so that getArgumentsString() doesn't return this line // during the tab-completion of the next command cmdLine = null; clear(Scope.REQUEST); } } // Method called for if condition and low level operation to be guarded by a timeout. @Override public ModelNode execute(Operation mn, String description) throws CommandLineException, IOException { if (client == null) { throw new CommandLineException("The connection to the controller " + "has not been established."); } try { return execute(() -> { return executor.execute(mn, timeout, TimeUnit.SECONDS); }, description); } catch (CommandLineException ex) { if (ex.getCause() instanceof IOException) { throw (IOException) ex.getCause(); } else { throw ex; } } } @Override public ModelNode execute(ModelNode mn, String description) throws CommandLineException, IOException { OperationBuilder opBuilder = new OperationBuilder(mn, true); return execute(opBuilder.build(), description); } // Single execute method to handle exceptions. private <T> T execute(Callable<T> c, String msg) throws CommandLineException { try { return c.call(); } catch (IOException ex) { throw new CommandLineException("IO exception for " + msg, ex); } catch (TimeoutException ex) { throw new CommandLineException("Timeout exception for " + msg, ex); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); throw new CommandLineException("Interrupt exception for " + msg, ex); } catch (ExecutionException ex) { Throwable cause = ex.getCause() == null ? ex : ex.getCause(); if (cause instanceof CommandLineException) { throw (CommandLineException) cause; } throw new CommandLineException("Execution exception for " + msg + ": " + cause.getMessage(), cause); } catch (CommandLineException ex) { throw ex; } catch (Exception ex) { throw new CommandLineException("Exception for " + msg, ex); } } public void handleSafe(String line) { exitCode = 0; try { handle(line); } catch(Throwable t) { error(Util.getMessagesFromThrowable(t)); } } @Override public String getArgumentsString() { // a little hack to support tab-completion of commands and ops spread across multiple lines if(lineBuffer != null) { return lineBuffer.toString(); } if (cmdLine != null && parsedCmd.getOperationName() != null) { int cmdNameLength = parsedCmd.getOperationName().length(); if (cmdLine.length() == cmdNameLength) { return null; } else { return cmdLine.substring(cmdNameLength + 1); } } return null; } @Override public void terminateSession() { if(terminate == RUNNING) { clear(Scope.CONTEXT); clear(Scope.REQUEST); terminate = TERMINATING; disconnectController(); restoreStdIO(); if(console != null) { console.stop(); } if (shutdownHook != null) { CliShutdownHook.remove(shutdownHook); } executor.cancel(); terminate = TERMINATED; } } @Override public void printLine(String message) { final Level logLevel; if(exitCode != 0) { logLevel = Level.ERROR; } else { logLevel = Level.INFO; } if(log.isEnabled(logLevel)) { log.log(logLevel, message); } if (outputTarget != null) { try { outputTarget.append(message); outputTarget.newLine(); outputTarget.flush(); } catch (IOException e) { System.err.println("Failed to print '" + message + "' to the output target: " + e.getLocalizedMessage()); } return; } if(!SILENT) { if (console != null) { console.print(message); console.printNewLine(); } else { // non-interactive mode cliPrintStream.println(message); } } } /** * Set the exit code of the process to indicate an error and output the error message. * * WARNING This method should only be called for unrecoverable errors as once the exit code is set subsequent operations may * not be possible. * * @param message The message to display. */ protected void error(String message) { this.exitCode = 1; printLine(message); } private String readLine(String prompt, boolean password) throws CommandLineException { // Only fail an interact if we're not in interactive. if(!INTERACT && ERROR_ON_INTERACT){ interactionDisabled(); } if (console == null) { initBasicConsole(null); } else if(!console.running()) { console.start(); } if (password) { return console.readLine(prompt, (char) 0x00); } else { return console.readLine(prompt); } } protected void interactionDisabled() throws CommandLineException { throw new CommandLineException("Invalid Usage. Prompt attempted in non-interactive mode. Please check commands or change CLI mode."); } @Override public void printColumns(Collection<String> col) { if(log.isInfoEnabled()) { log.info(col); } if (outputTarget != null) { try { for (String item : col) { outputTarget.append(item); outputTarget.newLine(); } } catch (IOException e) { System.err.println("Failed to print columns '" + col + "' to the console: " + e.getLocalizedMessage()); } return; } if(!SILENT) { if (console != null) { console.printColumns(col); } else { // non interactive mode for (String item : col) { cliPrintStream.println(item); } } } } @Override public void set(Scope scope, String key, Object value) { Objects.requireNonNull(scope); Objects.requireNonNull(key); Map<String, Object> store = map.get(scope); if (store == null) { store = new HashMap<>(); map.put(scope, store); } store.put(key, value); } @Override public Object get(Scope scope, String key) { Objects.requireNonNull(scope); Objects.requireNonNull(key); Map<String, Object> store = map.get(scope); Object value = null; if (store != null) { value = store.get(key); } return value; } @Override public void clear(Scope scope) { Objects.requireNonNull(scope); Map<String, Object> store = map.remove(scope); if (store != null) { store.clear(); } } @Override public Object remove(Scope scope, String key) { Objects.requireNonNull(scope); Map<String, Object> store = map.get(scope); Object value = null; if (store != null) { value = store.remove(key); } return value; } @Override public ModelControllerClient getModelControllerClient() { return client; } @Override public CommandLineParser getCommandLineParser() { return DefaultOperationRequestParser.INSTANCE; } @Override public OperationRequestAddress getCurrentNodePath() { return prefix; } @Override public NodePathFormatter getNodePathFormatter() { return prefixFormatter; } @Override public OperationCandidatesProvider getOperationCandidatesProvider() { return operationCandidatesProvider; } @Override public void connectController() throws CommandLineException { connectController(null); } @Override public void connectController(String controller) throws CommandLineException { ControllerAddress address = addressResolver.resolveAddress(controller); // In case the alias mappings cause us to enter some form of loop or a badly // configured server does the same, Set<ControllerAddress> visited = new HashSet<ControllerAddress>(); visited.add(address); boolean retry = false; do { try { CallbackHandler cbh = new AuthenticationCallbackHandler(username, password); if (log.isDebugEnabled()) { log.debug("connecting to " + address.getHost() + ':' + address.getPort() + " as " + username); } ModelControllerClient tempClient = ModelControllerClientFactory.CUSTOM.getClient(address, cbh, disableLocalAuth, sslContext, config.getConnectionTimeout(), this, timeoutHandler, clientBindAddress); retry = false; connInfoBean = new ConnectionInfoBean(); tryConnection(tempClient, address); initNewClient(tempClient, address, connInfoBean); connInfoBean.setDisableLocalAuth(disableLocalAuth); connInfoBean.setLoggedSince(new Date()); } catch (RedirectException re) { try { URI location = new URI(re.getLocation()); if (("remote+http".equals(address.getProtocol()) || "http-remoting".equals(address.getProtocol())) && "https".equals(location.getScheme())) { int port = location.getPort(); if (port < 0) { port = 443; } address = addressResolver.resolveAddress(new URI("remote+https", null, location.getHost(), port, null, null, null).toString()); if (visited.add(address) == false) { throw new CommandLineException("Redirect to address already tried encountered Address=" + address.toString()); } retry = true; } else if (address.getHost().equals(location.getHost()) && address.getPort() == location.getPort() && location.getPath() != null && location.getPath().length() > 1) { throw new CommandLineException("Server at " + address.getHost() + ":" + address.getPort() + " does not support " + address.getProtocol()); } else { throw new CommandLineException("Unsupported redirect received.", re); } } catch (URISyntaxException e) { throw new CommandLineException("Bad redirect location '" + re.getLocation() + "' received.", e); } } catch (IOException e) { throw new CommandLineException("Failed to resolve host '" + address.getHost() + "'", e); } } while (retry); } @Override @Deprecated public void connectController(String host, int port) throws CommandLineException { try { connectController(new URI(null, null, host, port, null, null, null).toString().substring(2)); } catch (URISyntaxException e) { throw new CommandLineException("Unable to construct URI for connection.", e); } } @Override public void bindClient(ModelControllerClient newClient) { ConnectionInfoBean conInfo = new ConnectionInfoBean(); conInfo.setLoggedSince(new Date()); initNewClient(newClient, null, conInfo); } private void initNewClient(ModelControllerClient newClient, ControllerAddress address, ConnectionInfoBean conInfo) { if (newClient != null) { if (this.client != null) { disconnectController(); } client = newClient; this.currentAddress = address; this.connInfoBean = conInfo; List<String> nodeTypes = Util.getNodeTypes(newClient, new DefaultOperationRequestAddress()); domainMode = nodeTypes.contains(Util.SERVER_GROUP); try { extLoader.loadHandlers(currentAddress); } catch (CommandLineException e) { printLine(Util.getMessagesFromThrowable(e)); } } } @Override public File getCurrentDir() { return currentDir; } @Override public void setCurrentDir(File dir) { if(dir == null) { throw new IllegalArgumentException("dir is null"); } this.currentDir = dir; } @Override public void registerRedirection(CommandLineRedirection redirection) throws CommandLineException { if(this.redirection != null) { throw new CommandLineException("Another redirection is currently active."); } this.redirection = new CommandLineRedirectionRegistration(redirection); redirection.set(this.redirection); } /** * Handle the last SSL failure, prompting the user to accept or reject the certificate of the remote server. * * @return true if the certificate validation should be retried. */ private void handleSSLFailure(Certificate[] lastChain) throws CommandLineException { printLine("Unable to connect due to unrecognised server certificate"); for (Certificate current : lastChain) { if (current instanceof X509Certificate) { X509Certificate x509Current = (X509Certificate) current; Map<String, String> fingerprints = FingerprintGenerator.generateFingerprints(x509Current); printLine("Subject - " + x509Current.getSubjectX500Principal().getName()); printLine("Issuer - " + x509Current.getIssuerDN().getName()); printLine("Valid From - " + x509Current.getNotBefore()); printLine("Valid To - " + x509Current.getNotAfter()); for (String alg : fingerprints.keySet()) { printLine(alg + " : " + fingerprints.get(alg)); } printLine(""); } } for (;;) { String response; if (trustManager.isModifyTrustStore()) { response = readLine("Accept certificate? [N]o, [T]emporarily, [P]ermanently : ", false); } else { response = readLine("Accept certificate? [N]o, [T]emporarily : ", false); } if (response == null) break; else if (response.length() == 1) { switch (response.toLowerCase(Locale.ENGLISH).charAt(0)) { case 'n': return; case 't': trustManager.storeChainTemporarily(lastChain); return; case 'p': if (trustManager.isModifyTrustStore()) { trustManager.storeChainPermenantly(lastChain); return; } } } } } /** * Used to make a call to the server to verify that it is possible to connect. */ private void tryConnection(final ModelControllerClient client, ControllerAddress address) throws CommandLineException, RedirectException { try { DefaultOperationRequestBuilder builder = new DefaultOperationRequestBuilder(); builder.setOperationName(Util.READ_ATTRIBUTE); builder.addProperty(Util.NAME, Util.NAME); final long start = System.currentTimeMillis(); final long timeoutMillis = config.getConnectionTimeout() + 1000; boolean tryConnection = true; while (tryConnection) { final ModelNode response = client.execute(builder.buildRequest()); if (!Util.isSuccess(response)) { // here we check whether the error is related to the access control permissions // WFLYCTL0332: Permission denied // WFLYCTL0313: Unauthorized to execute operation final String failure = Util.getFailureDescription(response); if (failure.contains("WFLYCTL0332")) { StreamUtils.safeClose(client); throw new CommandLineException( "Connection refused based on the insufficient user permissions." + " Please, make sure the security-realm attribute is specified for the relevant management interface" + " (standalone.xml/host.xml) and review the access-control configuration (standalone.xml/domain.xml)."); } else if (failure.contains("WFLYCTL0379")) { // system boot is in process if (System.currentTimeMillis() - start > timeoutMillis) { throw new CommandLineException("Timeout waiting for the system to boot."); } try { Thread.sleep(100); } catch (InterruptedException e) { disconnectController(); throw new CommandLineException("Interrupted while pausing before trying connection.", e); } } else { // Otherwise, on one hand, we don't actually care what the response is, // we just want to be sure the ModelControllerClient does not throw an Exception. // On the other hand, reading name attribute is a very basic one which should always work printLine("Warning! The connection check resulted in failure: " + Util.getFailureDescription(response)); tryConnection = false; } } else { tryConnection = false; } } } catch (Exception e) { try { Throwable current = e; while (current != null) { if (current instanceof SaslException) { throw new CommandLineException("Unable to authenticate against controller at " + address.getHost() + ":" + address.getPort(), current); } if (current instanceof SSLException) { throw new CommandLineException("Unable to negotiate SSL connection with controller at "+ address.getHost() + ":" + address.getPort()); } if (current instanceof RedirectException) { throw (RedirectException) current; } if (current instanceof CommandLineException) { throw (CommandLineException) current; } current = current.getCause(); } // We don't know what happened, most likely a timeout. throw new CommandLineException("The controller is not available at " + address.getHost() + ":" + address.getPort(), e); } finally { StreamUtils.safeClose(client); } } } @Override public void disconnectController() { if (this.client != null) { StreamUtils.safeClose(client); // if(loggingEnabled) { // printLine("Closed connection to " + this.controllerHost + ':' + // this.controllerPort); // } client = null; this.currentAddress = null; domainMode = false; notifyListeners(CliEvent.DISCONNECTED); connInfoBean = null; extLoader.resetHandlers(); } promptConnectPart = null; if(console != null && terminate == RUNNING) { console.setPrompt(getPrompt()); } } @Override @Deprecated public String getDefaultControllerHost() { return config.getDefaultControllerHost(); } @Override @Deprecated public int getDefaultControllerPort() { return config.getDefaultControllerPort(); } @Override public ControllerAddress getDefaultControllerAddress() { return config.getDefaultControllerAddress(); } @Override public String getControllerHost() { return currentAddress != null ? currentAddress.getHost() : null; } @Override public int getControllerPort() { return currentAddress != null ? currentAddress.getPort() : -1; } @Override public void clearScreen() { if(console != null) { console.clearScreen(); } } String promptConnectPart; String getPrompt() { if(lineBuffer != null) { return "> "; } StringBuilder buffer = new StringBuilder(); if (promptConnectPart == null) { buffer.append('['); String controllerHost = getControllerHost(); if (client != null) { if (domainMode) { buffer.append("domain@"); } else { buffer.append("standalone@"); } if (controllerHost != null) { buffer.append(controllerHost).append(':').append(getControllerPort()).append(' '); } else { buffer.append("embedded "); } promptConnectPart = buffer.toString(); } else { buffer.append("disconnected "); } } else { buffer.append(promptConnectPart); } if (prefix.isEmpty()) { buffer.append('/'); } else { buffer.append(prefix.getNodeType()); final String nodeName = prefix.getNodeName(); if (nodeName != null) { buffer.append('=').append(nodeName); } } if (isBatchMode()) { buffer.append(" #"); } buffer.append("] "); return buffer.toString(); } @Override public CommandHistory getHistory() { if(console == null) { try { initBasicConsole(null, INTERACT); } catch (CliInitializationException e) { throw new IllegalStateException("Failed to initialize console.", e); } } return console.getHistory(); } private void resetArgs(String cmdLine) throws CommandFormatException { if (cmdLine != null) { parsedCmd.parse(prefix, cmdLine, this); setOutputTarget(parsedCmd.getOutputTarget()); } this.cmdLine = cmdLine; } @Override public boolean isBatchMode() { return batchManager.isBatchActive(); } @Override public boolean isWorkflowMode() { return redirection != null; } @Override public BatchManager getBatchManager() { return batchManager; } @Override public BatchedCommand toBatchedCommand(String line) throws CommandFormatException { HandledRequest req = buildRequest(line, true); return new DefaultBatchedCommand(this, line, req.getRequest(), req.getResponseHandler()); } @Override public ModelNode buildRequest(String line) throws CommandFormatException { return buildRequest(line, false).getRequest(); } protected HandledRequest buildRequest(String line, boolean batchMode) throws CommandFormatException { if (line == null || line.isEmpty()) { throw new OperationFormatException("The line is null or empty."); } final DefaultCallbackHandler originalParsedArguments = this.parsedCmd; final String originalCmdLine = this.cmdLine; try { this.parsedCmd = new DefaultCallbackHandler(); resetArgs(line); if (parsedCmd.getFormat() == OperationFormat.INSTANCE) { final ModelNode request = this.parsedCmd.toOperationRequest(this); StringBuilder op = new StringBuilder(); op.append(prefixFormatter.format(parsedCmd.getAddress())); op.append(line.substring(line.indexOf(':'))); return new HandledRequest(request, null); } final CommandHandler handler = cmdRegistry.getCommandHandler(parsedCmd.getOperationName()); if (handler == null) { throw new OperationFormatException("No command handler for '" + parsedCmd.getOperationName() + "'."); } if(batchMode) { if(!handler.isBatchMode(this)) { throw new OperationFormatException("The command is not allowed in a batch."); } Batch batch = getBatchManager().getActiveBatch(); return ((OperationCommand) handler).buildHandledRequest(this, batch.getAttachments()); } else if (!(handler instanceof OperationCommand)) { throw new OperationFormatException("The command does not translate to an operation request."); } return new HandledRequest(((OperationCommand) handler).buildRequest(this), null); } finally { clear(Scope.REQUEST); this.parsedCmd = originalParsedArguments; this.cmdLine = originalCmdLine; } } @Override public CommandLineCompleter getDefaultCommandCompleter() { return cmdCompleter; } @Override public ParsedCommandLine getParsedCommandLine() { return parsedCmd; } @Override public boolean isDomainMode() { return domainMode; } @Override public void addEventListener(CliEventListener listener) { if (listener == null) { throw new IllegalArgumentException("Listener is null."); } listeners.add(listener); } @Override public CliConfig getConfig() { return config; } protected void setOutputTarget(String filePath) { if (filePath == null) { this.outputTarget = null; return; } try { this.outputTarget = Files.newBufferedWriter(Paths.get(filePath), StandardCharsets.UTF_8); } catch (IOException e) { error(e.getLocalizedMessage()); } } protected void notifyListeners(CliEvent event) { for (CliEventListener listener : listeners) { listener.cliEvent(event, this); } } @Override public void interact() { INTERACT = true; if(cmdCompleter == null) { throw new IllegalStateException("The console hasn't been initialized at construction time."); } if (this.client == null) { printLine("You are disconnected at the moment. Type 'connect' to connect to the server or" + " 'help' for the list of supported commands."); } console.setPrompt(getPrompt()); if(!console.running()) { console.start(); } // if console is already running before we have started interacting, we need to // make sure that the prompt is correctly displayed else { console.redrawPrompt(); } // We unlock the console for it to continue process inputstream. // Could have been put in controlled mode during username/password prompt. if(console.isControlled()) { console.continuous(); } while(/*!isTerminated() && */console.running()){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } INTERACT = false; } @Override public boolean isResolveParameterValues() { return resolveParameterValues; } @Override public void setResolveParameterValues(boolean resolve) { this.resolveParameterValues = resolve; } @Override public void handleClose() { // if the connection loss was triggered by an instruction to restart/reload // then we don't disconnect yet if(parsedCmd.getFormat() != null) { if(Util.RELOAD.equals(parsedCmd.getOperationName())) { // do nothing } else if(Util.SHUTDOWN.equals(parsedCmd.getOperationName())) { if(CommandFormat.INSTANCE.equals(parsedCmd.getFormat()) // shutdown command handler decides whether to disconnect or not || Util.TRUE.equals(parsedCmd.getPropertyValue(Util.RESTART))) { // do nothing } else { printLine(""); printLine("The controller has closed the connection."); disconnectController(); } } else { // we don't disconnect here because the connection may be closed by another // CLI instance/session doing a reload (this happens in our testsuite) } } else { // we don't disconnect here because the connection may be closed by another // CLI instance/session doing a reload (this happens in our testsuite) } } @Override public boolean isSilent() { return SILENT; } @Override public void setSilent(boolean silent) { SILENT = silent; } @Override public int getTerminalWidth() { if( !INTERACT ){ return 0; } if(console == null) { try { this.initBasicConsole(null); } catch (CliInitializationException e) { this.error("Failed to initialize the console: " + e.getLocalizedMessage()); return 80; } } return console.getTerminalWidth(); } @Override public int getTerminalHeight() { if( !INTERACT ){ return 0; } if(console == null) { try { this.initBasicConsole(null); } catch (CliInitializationException e) { this.error("Failed to initialize the console: " + e.getLocalizedMessage()); return 24; } } return console.getTerminalHeight(); } @Override public void setVariable(String name, String value) throws CommandLineException { if(name == null || name.isEmpty()) { throw new CommandLineException("Variable name can't be null or an empty string"); } if(!Character.isJavaIdentifierStart(name.charAt(0))) { throw new CommandLineException("Variable name must be a valid Java identifier (and not contain '$'): '" + name + "'"); } for(int i = 1; i < name.length(); ++i) { final char c = name.charAt(i); if(!Character.isJavaIdentifierPart(c) || c == '$') { throw new CommandLineException("Variable name must be a valid Java identifier (and not contain '$'): '" + name + "'"); } } if(value == null) { if(variables == null) { return; } variables.remove(name); } else { if(variables == null) { variables = new HashMap<String,String>(); } variables.put(name, value); } } @Override public String getVariable(String name) { return variables == null ? null : variables.get(name); } @Override public Collection<String> getVariables() { return variables == null ? Collections.<String>emptySet() : variables.keySet(); } private class AuthenticationCallbackHandler implements CallbackHandler { // After the CLI has connected the physical connection may be re-established numerous times. // for this reason we cache the entered values to allow for re-use without pestering the end // user. private String realm = null; private boolean realmShown = false; private String username; private char[] password; private String digest; private AuthenticationCallbackHandler(String username, char[] password) { // A local cache is used for scenarios where no values are specified on the command line // and the user wishes to use the connect command to establish a new connection. this.username = username; this.password = password; } private AuthenticationCallbackHandler(String username, String digest) { this.username = username; this.digest = digest; } @Override public void handle(final Callback[] callbacks) throws IOException, UnsupportedCallbackException { try { timeoutHandler.suspendAndExecute(new Runnable() { @Override public void run() { try { if (username == null || password == null) { // Only control the console if not already interacting. if (!INTERACT) { if (console == null) { initBasicConsole(null, false); } // That is required to stop the console from executing any command // before to be done with credentials. console.controlled(); if (!console.running()) { console.start(); } } } dohandle(callbacks); } catch (IOException | UnsupportedCallbackException | CliInitializationException e) { throw new RuntimeException(e); } } }); } catch (RuntimeException e) { if (e.getCause() instanceof IOException) { throw (IOException) e.getCause(); } else if (e.getCause() instanceof UnsupportedCallbackException) { throw (UnsupportedCallbackException) e.getCause(); } throw e; } } private void dohandle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { // Special case for anonymous authentication to avoid prompting user for their name. if (callbacks.length == 1 && callbacks[0] instanceof NameCallback) { ((NameCallback) callbacks[0]).setName("anonymous CLI user"); return; } for (Callback current : callbacks) { if (current instanceof RealmCallback) { RealmCallback rcb = (RealmCallback) current; String defaultText = rcb.getDefaultText(); realm = defaultText; rcb.setText(defaultText); // For now just use the realm suggested. } else if (current instanceof RealmChoiceCallback) { throw new UnsupportedCallbackException(current, "Realm choice not currently supported."); } else if (current instanceof NameCallback) { NameCallback ncb = (NameCallback) current; if (username == null) { showRealm(); try { if(console == null) { if(ERROR_ON_INTERACT) { interactionDisabled(); } initBasicConsole(null); } console.setCompletion(false); console.getHistory().setUseHistory(false); username = readLine("Username: ", false); console.getHistory().setUseHistory(true); console.setCompletion(true); } catch (CommandLineException e) { // the messages of the cause are lost if nested here throw new IOException("Failed to read username: " + e.getLocalizedMessage()); } if (username == null || username.length() == 0) { throw new SaslException("No username supplied."); } } connInfoBean.setUsername(username); ncb.setName(username); } else if (current instanceof PasswordCallback && digest == null) { // If a digest had been set support for PasswordCallback is disabled. if (password == null) { showRealm(); String temp; try { if(console == null) { if(ERROR_ON_INTERACT) { interactionDisabled(); } initBasicConsole(null); } console.setCompletion(false); console.getHistory().setUseHistory(false); temp = readLine("Password: ", true); console.getHistory().setUseHistory(true); console.setCompletion(true); } catch (CommandLineException e) { // the messages of the cause are lost if nested here throw new IOException("Failed to read password: " + e.getLocalizedMessage()); } if (temp != null) { password = temp.toCharArray(); } } PasswordCallback pcb = (PasswordCallback) current; pcb.setPassword(password); } else if (current instanceof CredentialCallback) { final CredentialCallback cc = (CredentialCallback) current; if (digest == null && cc.isCredentialTypeSupported(PasswordCredential.class, ClearPassword.ALGORITHM_CLEAR)) { if (password == null) { showRealm(); String temp; try { if(console == null) { if(ERROR_ON_INTERACT) { interactionDisabled(); } initBasicConsole(null); } console.setCompletion(false); console.getHistory().setUseHistory(false); temp = readLine("Password: ", true); console.getHistory().setUseHistory(true); console.setCompletion(true); } catch (CommandLineException e) { // the messages of the cause are lost if nested here throw new IOException("Failed to read password: " + e.getLocalizedMessage()); } if (temp != null) { password = temp.toCharArray(); } } cc.setCredential(new PasswordCredential(ClearPassword.createRaw(ClearPassword.ALGORITHM_CLEAR, password))); } else if (digest != null && cc.isCredentialTypeSupported(PasswordCredential.class, DigestPassword.ALGORITHM_DIGEST_MD5)) { // We don't support an interactive use of this callback so it must have been set in advance. final byte[] bytes = CodePointIterator.ofString(digest).hexDecode().drain(); cc.setCredential(new PasswordCredential(DigestPassword.createRaw(DigestPassword.ALGORITHM_DIGEST_MD5, username, realm, bytes))); } else { throw new UnsupportedCallbackException(current); } } else { throw new UnsupportedCallbackException(current); } } } private void showRealm() { if (realmShown == false && realm != null) { realmShown = true; printLine("Authenticating against security realm: " + realm); } } } /** * A trust manager that by default delegates to a lazily initialised TrustManager, this TrustManager also support both * temporarily and permanently accepting unknown server certificate chains. * * This class also acts as an aggregation of the configuration related to TrustStore handling. * * It is not intended that Certificate management requests occur if this class is registered to a SSLContext * with multiple concurrent clients. */ private class LazyDelagatingTrustManager implements X509TrustManager { // Configuration based state set on initialisation. private final String trustStore; private final String trustStorePassword; private final boolean modifyTrustStore; private Set<X509Certificate> temporarilyTrusted = new HashSet<X509Certificate>(); private X509TrustManager delegate; LazyDelagatingTrustManager(String trustStore, String trustStorePassword, boolean modifyTrustStore) { this.trustStore = trustStore; this.trustStorePassword = trustStorePassword; this.modifyTrustStore = modifyTrustStore; } /* * Methods to allow client interaction for certificate verification. */ boolean isModifyTrustStore() { return modifyTrustStore; } synchronized void storeChainTemporarily(final Certificate[] chain) { for (Certificate current : chain) { if (current instanceof X509Certificate) { temporarilyTrusted.add((X509Certificate) current); } } delegate = null; // Triggers a reload on next use. } synchronized void storeChainPermenantly(final Certificate[] chain) { FileInputStream fis = null; FileOutputStream fos = null; try { KeyStore theTrustStore = KeyStore.getInstance("JKS"); File trustStoreFile = new File(trustStore); if (trustStoreFile.exists()) { fis = new FileInputStream(trustStoreFile); theTrustStore.load(fis, trustStorePassword.toCharArray()); StreamUtils.safeClose(fis); fis = null; } else { theTrustStore.load(null); } for (Certificate current : chain) { if (current instanceof X509Certificate) { X509Certificate x509Current = (X509Certificate) current; theTrustStore.setCertificateEntry(x509Current.getSubjectX500Principal().getName(), x509Current); } } fos = new FileOutputStream(trustStoreFile); theTrustStore.store(fos, trustStorePassword.toCharArray()); } catch (GeneralSecurityException e) { throw new IllegalStateException("Unable to operate on trust store.", e); } catch (IOException e) { throw new IllegalStateException("Unable to operate on trust store.", e); } finally { StreamUtils.safeClose(fis); StreamUtils.safeClose(fos); } delegate = null; // Triggers a reload on next use. } /* * Internal Methods */ private synchronized X509TrustManager getDelegate() { if (delegate == null) { FileInputStream fis = null; try { KeyStore theTrustStore = KeyStore.getInstance("JKS"); File trustStoreFile = new File(trustStore); if (trustStoreFile.exists()) { fis = new FileInputStream(trustStoreFile); theTrustStore.load(fis, trustStorePassword.toCharArray()); } else { theTrustStore.load(null); } for (X509Certificate current : temporarilyTrusted) { theTrustStore.setCertificateEntry(current.getSubjectX500Principal().getName(), current); } TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(theTrustStore); TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); for (TrustManager current : trustManagers) { if (current instanceof X509TrustManager) { delegate = (X509TrustManager) current; break; } } } catch (GeneralSecurityException e) { throw new IllegalStateException("Unable to operate on trust store.", e); } catch (IOException e) { throw new IllegalStateException("Unable to operate on trust store.", e); } finally { StreamUtils.safeClose(fis); } } if (delegate == null) { throw new IllegalStateException("Unable to create delegate trust manager."); } return delegate; } /* * X509TrustManager Methods */ @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { // The CLI is only verifying servers. getDelegate().checkClientTrusted(chain, authType); } @Override public void checkServerTrusted(final X509Certificate[] chain, String authType) throws CertificateException { boolean retry; do { retry = false; try { getDelegate().checkServerTrusted(chain, authType); connInfoBean.setServerCertificates(chain); } catch (CertificateException ce) { if (retry == false) { timeoutHandler.suspendAndExecute(new Runnable() { @Override public void run() { try { handleSSLFailure(chain); } catch (CommandLineException e) { throw new RuntimeException(e); } } }); if (delegate == null) { retry = true; } else { throw ce; } } else { throw ce; } } } while (retry); } @Override public X509Certificate[] getAcceptedIssuers() { return getDelegate().getAcceptedIssuers(); } } private class JaasConfigurationWrapper extends Configuration { private final Configuration wrapped; private JaasConfigurationWrapper(Configuration toWrap) { this.wrapped = toWrap; } @Override public AppConfigurationEntry[] getAppConfigurationEntry(String name) { AppConfigurationEntry[] response = wrapped != null ? wrapped.getAppConfigurationEntry(name) : null; if (response == null) { if ("com.sun.security.jgss.initiate".equals(name)) { HashMap<String, String> options = new HashMap<String, String>(2); options.put("useTicketCache", "true"); options.put("doNotPrompt", "true"); response = new AppConfigurationEntry[] { new AppConfigurationEntry( "com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options) }; } } return response; } } class CommandLineRedirectionRegistration implements CommandLineRedirection.Registration { CommandLineRedirection target; CommandLineRedirectionRegistration(CommandLineRedirection redirection) { if(redirection == null) { throw new IllegalArgumentException("Redirection is null"); } this.target = redirection; } @Override public void unregister() throws CommandLineException { ensureActive(); CommandContextImpl.this.redirection = null; } @Override public boolean isActive() { return CommandContextImpl.this.redirection == this; } @Override public void handle(ParsedCommandLine parsedLine) throws CommandLineException { ensureActive(); final String line = parsedLine.getSubstitutedLine(); try { if (parsedLine.getFormat() == OperationFormat.INSTANCE) { if (isBatchMode()) { Batch batch = getBatchManager().getActiveBatch(); final ModelNode request = Util.toOperationRequest(CommandContextImpl.this, parsedCmd, batch.getAttachments()); StringBuilder op = new StringBuilder(); op.append(getNodePathFormatter().format(parsedCmd.getAddress())); op.append(line.substring(line.indexOf(':'))); DefaultBatchedCommand batchedCmd = new DefaultBatchedCommand(CommandContextImpl.this, op.toString(), request, null); batch.add(batchedCmd); } else { Attachments attachments = new Attachments(); final ModelNode op = Util.toOperationRequest(CommandContextImpl.this, parsedCmd, attachments); RequestWithAttachments req = new RequestWithAttachments(op, attachments); set(Scope.REQUEST, "OP_REQ", req); operationHandler.handle(CommandContextImpl.this); } } else { final String cmdName = parsedCmd.getOperationName(); CommandHandler handler = cmdRegistry.getCommandHandler(cmdName.toLowerCase()); if (handler != null) { if (isBatchMode() && handler.isBatchMode(CommandContextImpl.this)) { if (!(handler instanceof OperationCommand)) { throw new CommandLineException("The command is not allowed in a batch."); } else { try { Batch batch = getBatchManager().getActiveBatch(); HandledRequest request = ((OperationCommand) handler). buildHandledRequest(CommandContextImpl.this, batch.getAttachments()); BatchedCommand batchedCmd = new DefaultBatchedCommand(CommandContextImpl.this, line, request.getRequest(), request.getResponseHandler()); batch.add(batchedCmd); } catch (CommandFormatException e) { throw new CommandFormatException("Failed to add to batch '" + line + "'", e); } } } else { handler.handle(CommandContextImpl.this); } } else { throw new CommandLineException("Unexpected command '" + line + "'. Type 'help --commands' for the list of supported commands."); } } } finally { clear(Scope.REQUEST); } } private void ensureActive() throws CommandLineException { if(!isActive()) { throw new CommandLineException("The redirection is not registered any more."); } } } public ConnectionInfo getConnectionInfo() { return connInfoBean; } @Override public void captureOutput(PrintStream captor) { assert captor != null; cliPrintStream.captureOutput(captor); } @Override public void releaseOutput() { cliPrintStream.releaseOutput(); } @Override public final void setCommandTimeout(int numSeconds) { if (numSeconds < 0) { throw new IllegalArgumentException("The command-timeout must be a " + "valid positive integer:" + numSeconds); } this.timeout = numSeconds; } @Override public final int getCommandTimeout() { return timeout; } @Override public final void resetTimeout(TIMEOUT_RESET_VALUE value) { switch (value) { case CONFIG: { timeout = configTimeout; break; } case DEFAULT: { timeout = DEFAULT_TIMEOUT; break; } } } }