/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.embedded.ssh.internal;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback;
import org.apache.sshd.server.SessionAware;
import org.apache.sshd.server.session.ServerSession;
import de.rcenvironment.core.command.api.CommandExecutionResult;
import de.rcenvironment.core.command.api.CommandExecutionService;
import de.rcenvironment.core.embedded.ssh.api.SshAccount;
import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription;
/**
* Class for handling command execution. Does not handle SCP-Commands.
*
* @author Sebastian Holtappels
* @author Robert Mischke
*/
public class SshCommandHandler implements Command, Runnable, SessionAware {
private InputStream in;
private String sshCommand;
private String loginName;
private ExitCallback callback;
private Environment environment;
private SshAuthenticationManager authenticationManager;
private CommandExecutionService commandExecutionService;
private SshConsoleOutputAdapter outputAdapter;
private final Log logger = LogFactory.getLog(getClass());
private SshAccount userAccount;
public SshCommandHandler(String sshCommand, SshAuthenticationManager authenticationManager,
CommandExecutionService commandExecutionService, SshConfiguration sshConfiguration) {
this.sshCommand = sshCommand;
this.authenticationManager = authenticationManager;
this.commandExecutionService = commandExecutionService;
this.outputAdapter = new SshConsoleOutputAdapter(commandExecutionService);
}
// Handling the thread - START
@Override
public void start(Environment env) throws IOException {
// start thread
environment = env;
loginName = environment.getEnv().get(Environment.ENV_USER);
userAccount = authenticationManager.getAccountByLoginName(loginName, false); // false = do not allow disabled
if (userAccount == null) {
outputAdapter.addOutput("Invalid/unknown login name: " + loginName);
logger.warn("Blocked unrecognized SSH account " + loginName);
callback.onExit(0);
}
// initialize console (if user is not a temp user)
if (isPotentiallyAllowedToRunCommands()) {
outputAdapter.setActiveUser(loginName);
// TODO review: thread safety? - misc_ro
ConcurrencyUtils.getAsyncTaskService().execute(this);
} else {
outputAdapter.addOutput("Your account is not allowed to run an interactive shell or execute commands.");
logger.warn("Blocked command/shell access for account " + loginName);
callback.onExit(0);
}
}
private boolean isPotentiallyAllowedToRunCommands() {
// TODO temporarily allowed for both account types; review
return true;
// return activeUser.startsWith(SshConstants.TEMP_USER_PREFIX);
}
@Override
@TaskDescription("SSH command session")
public void run() {
try {
if (sshCommand == null) {
// run interactive shell/console
logger.debug(StringUtils.format("Starting interactive shell for user \"%s\"", loginName));
runInteractiveShellLoop();
callback.onExit(0);
logger.debug(StringUtils.format("Finished interactive shell for user \"%s\"", loginName));
} else {
// execute single provided command and exit
// note: this logs all incoming commands, but at the current time, DEBUG level logging is considered to be safe (ie not
// accessible from remote nodes)
logger.debug(StringUtils.format("Starting command execution for user \"%s\": %s", loginName, sshCommand));
CommandExecutionResult result = executeSingleCommand(sshCommand);
switch (result) {
case DEFAULT:
case EXIT_REQUESTED:
callback.onExit(0);
break;
case ERROR:
case INTERRUPTED:
callback.onExit(1);
break;
default:
throw new IllegalArgumentException();
}
logger.debug(StringUtils.format("Finished command execution for user \"%s\": %s", loginName, sshCommand));
}
} catch (IOException e) {
// not logging the full stacktrace as it is usually irrelevant, and this case happens frequently
logger.error(StringUtils.format("I/O error in SSH session - the client may have closed the connection (user \"%s\"): %s",
loginName, e.toString()));
callback.onExit(1);
}
// End Console (Closes the connection)
}
@Override
public void destroy() {
// close resources
try {
in.close();
} catch (IOException e) {
logger.error("Could not close input stream. ", e);
}
outputAdapter.destroy();
// End thread and console
callback.onExit(0);
}
// Handling the thread - END
// Handling the commands - START
private void runInteractiveShellLoop() throws IOException {
BufferedReader r = new BufferedReader(new InputStreamReader(in));
InteractiveShellHandler commandBuffer = new InteractiveShellHandler(outputAdapter);
outputAdapter.printWelcome();
outputAdapter.printConsolePrompt();
while (true) {
int newCharCode = r.read();
// stop reading and start executing on 13 no matter what
if (newCharCode == SshConstants.RETURN_KEY_CODE) {
String command = commandBuffer.getCurrentCommand();
outputAdapter.addOutput("", true, false);
CommandExecutionResult result = executeSingleCommand(command);
if (result == CommandExecutionResult.EXIT_REQUESTED) {
return;
}
outputAdapter.printConsolePrompt();
} else {
if (commandBuffer.processInputChar(newCharCode)) {
if (newCharCode != SshConstants.DEL_KEY_CODE) {
outputAdapter.addOutput("" + (char) newCharCode, false, false);
} else {
outputAdapter.addOutput("\b \b", false, false);
}
}
}
}
}
private CommandExecutionResult executeSingleCommand(String command) throws IOException {
if (command != null && !command.isEmpty()) {
if (authenticationManager.isAllowedToExecuteConsoleCommand(loginName, command)) {
if (command.equalsIgnoreCase(SshConstants.EXIT_COMMAND)) {
// stop interactive shell on exit command
return CommandExecutionResult.EXIT_REQUESTED;
} else {
// TODO pass invoker information for non-temporary accounts as well
return sendToExecutionService(command, userAccount);
}
} else {
logger.debug("User " + loginName + " tried to execute command " + command
+ ". Attempt was blocked because of missing role privileges.");
outputAdapter.addOutput("\r\nCommand " + command
+ " not executed. You either do not have the privileges to execute this command or it does not exist.", true, false);
return CommandExecutionResult.ERROR; // TODO add more specific result?
}
} else {
return CommandExecutionResult.DEFAULT;
}
}
private CommandExecutionResult sendToExecutionService(String command, Object invokerInfo) {
List<String> tokens;
if (command.startsWith("rce")) {
command = command.replace("rce", "");
}
tokens = Arrays.asList(command.trim().split("\\s+"));
Future<CommandExecutionResult> resultFuture = commandExecutionService.asyncExecMultiCommand(tokens, outputAdapter, invokerInfo);
try {
// wait for termination
return resultFuture.get();
} catch (InterruptedException e) {
outputAdapter.addOutput("Command execution interrupted: " + e.toString());
logger.error("Interrupted while waiting for asynchronous command to finish", e);
return CommandExecutionResult.INTERRUPTED;
} catch (ExecutionException e) {
outputAdapter.addOutput("Error during command execution: " + e.toString());
logger.error("Error during command execution", e);
return CommandExecutionResult.ERROR;
}
}
// Handling the commands - END
// Getter and Setter - START
@Override
public void setInputStream(InputStream inParam) {
this.in = inParam;
}
@Override
public void setOutputStream(OutputStream out) {
outputAdapter.setOutputStream(out);
}
@Override
public void setErrorStream(OutputStream err) {
outputAdapter.setErrorStream(err);
}
@Override
public void setExitCallback(ExitCallback callbackParam) {
this.callback = callbackParam;
}
@Override
public void setSession(ServerSession sessionParam) {}
// Getter and Setter - END
}