All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.fonimus.ssh.shell.SshShellCommandFactory Maven / Gradle / Ivy

The newest version!
package com.github.fonimus.ssh.shell;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;

import org.apache.sshd.common.Factory;
import org.apache.sshd.common.channel.PtyMode;
import org.apache.sshd.server.ChannelSessionAware;
import org.apache.sshd.server.ExitCallback;
import org.apache.sshd.server.Signal;
import org.apache.sshd.server.channel.ChannelSession;
import org.apache.sshd.server.command.Command;
import org.jline.reader.EndOfFileException;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.Parser;
import org.jline.terminal.Attributes;
import org.jline.terminal.Size;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.Banner;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.shell.ExitRequest;
import org.springframework.shell.Input;
import org.springframework.shell.Shell;
import org.springframework.shell.jline.InteractiveShellApplicationRunner;
import org.springframework.shell.jline.JLineShellAutoConfiguration;
import org.springframework.shell.jline.PromptProvider;
import org.springframework.shell.result.DefaultResultHandler;
import org.springframework.stereotype.Component;

import com.github.fonimus.ssh.shell.auth.SshAuthentication;
import com.github.fonimus.ssh.shell.auth.SshShellSecurityAuthenticationProvider;

import static com.github.fonimus.ssh.shell.SshShellHistoryAutoConfiguration.HISTORY_FILE;

/**
 * Ssh shell command factory implementation
 */
@Component
public class SshShellCommandFactory
		implements Command, Factory, ChannelSessionAware, Runnable {

	public static final ThreadLocal SSH_THREAD_CONTEXT = ThreadLocal.withInitial(() -> null);

	private static final Logger LOGGER = LoggerFactory.getLogger(SshShellCommandFactory.class);

	private InputStream is;

	private OutputStream os;

	private ExitCallback ec;

	private Thread sshThread;

	private ChannelSession session;

	private Banner shellBanner;

	private PromptProvider promptProvider;

	private Shell shell;

	private JLineShellAutoConfiguration.CompleterAdapter completerAdapter;

	private final Parser parser;

	private Environment environment;

	private File historyFile;

	private org.apache.sshd.server.Environment sshEnv;

	/**
	 * Constructor
	 *
	 * @param banner           shell banner
	 * @param promptProvider   prompt provider
	 * @param shell            spring shell
	 * @param completerAdapter completer adapter
	 * @param parser           jline parser
	 * @param environment      spring environment
	 * @param historyFile      history file location
	 */
	public SshShellCommandFactory(@Autowired(required = false) Banner banner, @Lazy PromptProvider promptProvider, Shell shell,
			JLineShellAutoConfiguration.CompleterAdapter completerAdapter, Parser parser, Environment environment,
			@Qualifier(HISTORY_FILE) File historyFile) {
		this.shellBanner = banner;
		this.promptProvider = promptProvider;
		this.shell = shell;
		this.completerAdapter = completerAdapter;
		this.parser = parser;
		this.environment = environment;
		this.historyFile = historyFile;
	}

	/**
	 * Start ssh session
	 *
	 * @param env ssh environment
	 */
	@Override
	public void start(org.apache.sshd.server.Environment env) {
		LOGGER.debug("{}: start", session.toString());
		sshEnv = env;
		sshThread = new Thread(this, "ssh-session-" + System.nanoTime());
		sshThread.start();
	}

	/**
	 * Run ssh session
	 */
	@Override
	public void run() {
		LOGGER.debug("{}: run", session.toString());
		Size size = new Size(Integer.parseInt(sshEnv.getEnv().get("COLUMNS")), Integer.parseInt(sshEnv.getEnv().get("LINES")));
		try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
				PrintStream ps = new PrintStream(baos, true, StandardCharsets.UTF_8.name());
				Terminal terminal = TerminalBuilder.builder().system(false).size(size).type(sshEnv.getEnv().get("TERM")).streams(is, os).build()) {

			DefaultResultHandler resultHandler = new DefaultResultHandler();
			resultHandler.setTerminal(terminal);

			Attributes attr = terminal.getAttributes();
			fill(attr, sshEnv.getPtyModes());
			terminal.setAttributes(attr);

			sshEnv.addSignalListener(signal -> {
				terminal.setSize(new Size(
						Integer.parseInt(sshEnv.getEnv().get("COLUMNS")),
						Integer.parseInt(sshEnv.getEnv().get("LINES"))));
				terminal.raise(Terminal.Signal.WINCH);
			}, Signal.WINCH);

			if (shellBanner != null) {
				shellBanner.printBanner(environment, this.getClass(), ps);
			}
			resultHandler.handleResult(new String(baos.toByteArray(), StandardCharsets.UTF_8));
			resultHandler.handleResult("Please type `help` to see available commands");

			LineReader reader = LineReaderBuilder.builder()
					.terminal(terminal)
					.appName("Spring Ssh Shell")
					.completer(completerAdapter)
					.highlighter((reader1, buffer) -> {
						int l = 0;
						String best = null;
						for (String command : shell.listCommands().keySet()) {
							if (buffer.startsWith(command) && command.length() > l) {
								l = command.length();
								best = command;
							}
						}
						if (best != null) {
							return new AttributedStringBuilder(buffer.length()).append(best, AttributedStyle.BOLD).append(buffer.substring(l)).toAttributedString();
						} else {
							return new AttributedString(buffer, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
						}
					})
					.parser(parser)
					.build();
			reader.setVariable(LineReader.HISTORY_FILE, historyFile.toPath());

			Object authenticationObject = session.getSession().getIoSession().getAttribute(
					SshShellSecurityAuthenticationProvider.AUTHENTICATION_ATTRIBUTE);
			SshAuthentication authentication = null;
			if (authenticationObject != null) {
				if (!(authenticationObject instanceof SshAuthentication)) {
					throw new IllegalStateException("Unknown authentication object class: " + authenticationObject.getClass().getName());
				}
				authentication = (SshAuthentication) authenticationObject;
			}

			SSH_THREAD_CONTEXT.set(new SshContext(sshThread, terminal, reader, authentication));
			shell.run(new SshShellInputProvider(reader, promptProvider));
			LOGGER.debug("{}: end", session.toString());
			quit(0);
		} catch (IOException | RuntimeException e) {
			LOGGER.error("{}: unexpected exception", session.toString(), e);
			quit(1);
		}
	}

	private void fill(Attributes attr, Map ptyModes) {
		for (Map.Entry e : ptyModes.entrySet()) {
			switch (e.getKey()) {
			case VINTR:
				attr.setControlChar(Attributes.ControlChar.VINTR, e.getValue());
				break;
			case VQUIT:
				attr.setControlChar(Attributes.ControlChar.VQUIT, e.getValue());
				break;
			case VERASE:
				attr.setControlChar(Attributes.ControlChar.VERASE, e.getValue());
				break;
			case VKILL:
				attr.setControlChar(Attributes.ControlChar.VKILL, e.getValue());
				break;
			case VEOF:
				attr.setControlChar(Attributes.ControlChar.VEOF, e.getValue());
				break;
			case VEOL:
				attr.setControlChar(Attributes.ControlChar.VEOL, e.getValue());
				break;
			case VEOL2:
				attr.setControlChar(Attributes.ControlChar.VEOL2, e.getValue());
				break;
			case VSTART:
				attr.setControlChar(Attributes.ControlChar.VSTART, e.getValue());
				break;
			case VSTOP:
				attr.setControlChar(Attributes.ControlChar.VSTOP, e.getValue());
				break;
			case VSUSP:
				attr.setControlChar(Attributes.ControlChar.VSUSP, e.getValue());
				break;
			case VDSUSP:
				attr.setControlChar(Attributes.ControlChar.VDSUSP, e.getValue());
				break;
			case VREPRINT:
				attr.setControlChar(Attributes.ControlChar.VREPRINT, e.getValue());
				break;
			case VWERASE:
				attr.setControlChar(Attributes.ControlChar.VWERASE, e.getValue());
				break;
			case VLNEXT:
				attr.setControlChar(Attributes.ControlChar.VLNEXT, e.getValue());
				break;
			/*
			case VFLUSH:
					attr.setControlChar(Attributes.ControlChar.VMIN, e.getValue());
					break;
			case VSWTCH:
					attr.setControlChar(Attributes.ControlChar.VTIME, e.getValue());
					break;
			*/
			case VSTATUS:
				attr.setControlChar(Attributes.ControlChar.VSTATUS, e.getValue());
				break;
			case VDISCARD:
				attr.setControlChar(Attributes.ControlChar.VDISCARD, e.getValue());
				break;
			case ECHO:
				attr.setLocalFlag(Attributes.LocalFlag.ECHO, e.getValue() != 0);
				break;
			case ICANON:
				attr.setLocalFlag(Attributes.LocalFlag.ICANON, e.getValue() != 0);
				break;
			case ISIG:
				attr.setLocalFlag(Attributes.LocalFlag.ISIG, e.getValue() != 0);
				break;
			case ICRNL:
				attr.setInputFlag(Attributes.InputFlag.ICRNL, e.getValue() != 0);
				break;
			case INLCR:
				attr.setInputFlag(Attributes.InputFlag.INLCR, e.getValue() != 0);
				break;
			case IGNCR:
				attr.setInputFlag(Attributes.InputFlag.IGNCR, e.getValue() != 0);
				break;
			case OCRNL:
				attr.setOutputFlag(Attributes.OutputFlag.OCRNL, e.getValue() != 0);
				break;
			case ONLCR:
				attr.setOutputFlag(Attributes.OutputFlag.ONLCR, e.getValue() != 0);
				break;
			case ONLRET:
				attr.setOutputFlag(Attributes.OutputFlag.ONLRET, e.getValue() != 0);
				break;
			case OPOST:
				attr.setOutputFlag(Attributes.OutputFlag.OPOST, e.getValue() != 0);
				break;
			default:
				// nothing
			}
		}
	}

	private void quit(int exitCode) {
		ec.onExit(exitCode);
	}

	@Override
	public void destroy() {
		// nothing to do
	}

	class SshShellInputProvider
			extends InteractiveShellApplicationRunner.JLineInputProvider {

		public SshShellInputProvider(LineReader lineReader, PromptProvider promptProvider) {
			super(lineReader, promptProvider);
		}

		@Override
		public Input readInput() {
			SshContext ctx = SSH_THREAD_CONTEXT.get();
			if (ctx != null) {
				ctx.setPostProcessorsList(null);
			}
			try {
				return super.readInput();
			} catch (EndOfFileException e) {
				throw new ExitRequest(1);
			}
		}
	}

	@Override
	public void setErrorStream(OutputStream errOS) {
		// not used
	}

	@Override
	public void setExitCallback(ExitCallback ec) {
		this.ec = ec;
	}

	@Override
	public void setInputStream(InputStream is) {
		this.is = is;
	}

	@Override
	public void setOutputStream(OutputStream os) {
		this.os = os;
	}

	@Override
	public void setChannelSession(ChannelSession session) {
		this.session = session;
	}

	@Override
	public Command create() {
		return this;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy