Wouldn’t it be nice to have a possibility to connect you application securely via SSH with a secret user/password and communicate with it within the application specific shell? That was my first idea as I found the Apache’s SSHD project.
Apache SSHD is a 100% pure java library to support the SSH protocols on both the client and server side. This library is based on Apache MINA, a scalable and high performance asynchronous IO library.
This blog post demonstrates how is it possible to create a application shell using Apache SSHD and JLine projects.
mvn archetype:generate \ -DgroupId=net.javaforge.blog \ -DartifactId=sshd-daemon-demo \ -DarchetypeArtifactId=maven-archetype-quickstart \ -DinteractiveMode=false
<dependency> <groupId>org.apache.sshd</groupId> <artifactId>sshd-core</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>jline</groupId> <artifactId>jline</artifactId> <version>2.11</version> </dependency> <!-- not really necessary, just to simplify logging in this demo --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.6.6</version> </dependency>
public class InAppPasswordAuthenticator implements PasswordAuthenticator { @Override public boolean authenticate(String username, String password, ServerSession session) { return username != null && username.equals(password); } }
public class InAppShellFactory implements Factory { public Command create() { return new InAppShell(); } private static class InAppShell implements Command, Runnable { private static final Logger log = LoggerFactory.getLogger(InAppShell.class); public static final boolean IS_MAC_OSX = System.getProperty("os.name").startsWith("Mac OS X"); private static final String SHELL_THREAD_NAME = "InAppShell"; private static final String SHELL_PROMPT = "app> "; private static final String SHELL_CMD_QUIT = "quit"; private static final String SHELL_CMD_EXIT = "exit"; private static final String SHELL_CMD_VERSION = "version"; private static final String SHELL_CMD_HELP = "help"; private InputStream in; private OutputStream out; private OutputStream err; private ExitCallback callback; private Environment environment; private Thread thread; private ConsoleReader reader; private PrintWriter writer; public InputStream getIn() { return in; } public OutputStream getOut() { return out; } public OutputStream getErr() { return err; } public Environment getEnvironment() { return environment; } public void setInputStream(InputStream in) { this.in = in; } public void setOutputStream(OutputStream out) { this.out = out; } public void setErrorStream(OutputStream err) { this.err = err; } public void setExitCallback(ExitCallback callback) { this.callback = callback; } public void start(Environment env) throws IOException { environment = env; thread = new Thread(this, SHELL_THREAD_NAME); thread.start(); } public void destroy() { if (reader != null) reader.shutdown(); thread.interrupt(); } @Override public void run() { try { reader = new ConsoleReader(in, new FilterOutputStream(out) { @Override public void write(final int i) throws IOException { super.write(i); // workaround for MacOSX!! reset line after CR.. if (IS_MAC_OSX && i == ConsoleReader.CR.toCharArray()[0]) { super.write(ConsoleReader.RESET_LINE); } } }); reader.setPrompt(SHELL_PROMPT); reader.addCompleter(new StringsCompleter(SHELL_CMD_QUIT, SHELL_CMD_EXIT, SHELL_CMD_VERSION, SHELL_CMD_HELP)); writer = new PrintWriter(reader.getOutput()); // output welcome banner on ssh session startup writer.println("****************************************************"); writer.println("* Welcome to Application Shell. *"); writer.println("****************************************************"); writer.flush(); String line; while ((line = reader.readLine()) != null) { handleUserInput(line.trim()); } } catch (InterruptedIOException e) { // Ignore } catch (Exception e) { log.error("Error executing InAppShell...", e); } finally { callback.onExit(0); } } private void handleUserInput(String line) throws InterruptedIOException { if (line.equalsIgnoreCase(SHELL_CMD_QUIT) || line.equalsIgnoreCase(SHELL_CMD_EXIT)) throw new InterruptedIOException(); String response; if (line.equalsIgnoreCase(SHELL_CMD_VERSION)) response = "InApp version 1.0.0"; else if (line.equalsIgnoreCase(SHELL_CMD_HELP)) response = "Help is not implemented yet..."; else response = "======> \"" + line + "\""; writer.println(response); writer.flush(); } } }
public class App { public static void main(String[] args) throws Throwable { SshServer sshd = SshServer.setUpDefaultServer(); sshd.setPort(5222); sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider("hostkey.ser")); sshd.setPasswordAuthenticator(new InAppPasswordAuthenticator()); sshd.setShellFactory(new InAppShellFactory()); sshd.start(); } }
The KeyPairProvider above is used on the server side to provide the host key.
Now after starting our SSH daemon we can connect it using ordinary SSH client. Find below a screenshot of my session with the InAppShell:
The project source code is hosted on GitHub under https://github.com/bigpuritz/javaforge-blog/tree/master/sshd-daemon-demo