diff --git a/recipes/pkg/ssh_terminal/README b/recipes/pkg/ssh_terminal/README
new file mode 100644
index 0000000..735739a
--- /dev/null
+++ b/recipes/pkg/ssh_terminal/README
@@ -0,0 +1,14 @@
+
+ SSH terminal client
+
+A Nitpicker terminal that connects to a shell on a remote SSH server.
+
+It is configured with a "host" file found in its root directory with the
+following format:
+
+
+The port, pass, and known attributes are optional. The client will first
+try to authenticate with a keypair found in the root directory with a
+fallback to password authentication. The client will automatically
+disconnect from hosts that are not found in "/known_hosts", unless the
+"known" attribute is set to a false in the host file.
diff --git a/recipes/pkg/ssh_terminal/archives b/recipes/pkg/ssh_terminal/archives
new file mode 100644
index 0000000..28c298f
--- /dev/null
+++ b/recipes/pkg/ssh_terminal/archives
@@ -0,0 +1,10 @@
+_/pkg/terminal
+_/src/libc
+_/src/libcrypto
+_/src/nit_fb
+_/src/vfs
+_/src/zlib
+_/src/vfs_jitterentropy
+_/src/libssh
+_/src/ssh_client
+_/src/vfs_lwip
diff --git a/recipes/pkg/ssh_terminal/hash b/recipes/pkg/ssh_terminal/hash
new file mode 100644
index 0000000..1d6b0ea
--- /dev/null
+++ b/recipes/pkg/ssh_terminal/hash
@@ -0,0 +1 @@
+2018-07-17-k fb159d9fe48bb3081f79f0d78e4b41dba0a164a4
diff --git a/recipes/pkg/ssh_terminal/runtime b/recipes/pkg/ssh_terminal/runtime
new file mode 100644
index 0000000..1dd2afb
--- /dev/null
+++ b/recipes/pkg/ssh_terminal/runtime
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/recipes/src/ssh_client/content.mk b/recipes/src/ssh_client/content.mk
new file mode 100644
index 0000000..865d866
--- /dev/null
+++ b/recipes/src/ssh_client/content.mk
@@ -0,0 +1,2 @@
+SRC_DIR = src/app/ssh_client
+include $(GENODE_DIR)/repos/base/recipes/src/content.inc
diff --git a/recipes/src/ssh_client/hash b/recipes/src/ssh_client/hash
new file mode 100644
index 0000000..80391ce
--- /dev/null
+++ b/recipes/src/ssh_client/hash
@@ -0,0 +1 @@
+2018-07-17 54242fdfe92139b894d789ecb438f632d1214ff9
diff --git a/recipes/src/ssh_client/used_apis b/recipes/src/ssh_client/used_apis
new file mode 100644
index 0000000..75c44c8
--- /dev/null
+++ b/recipes/src/ssh_client/used_apis
@@ -0,0 +1,6 @@
+base
+libc
+libssh
+os
+terminal_session
+vfs
diff --git a/run/ssh_client.run b/run/ssh_client.run
new file mode 100644
index 0000000..2b12d35
--- /dev/null
+++ b/run/ssh_client.run
@@ -0,0 +1,177 @@
+#
+# \brief Test of ssh_client
+# \author Emery Hemingway
+#
+
+if {[have_spec odroid_xu] || [have_spec linux] ||
+ [expr [have_spec imx53] && [have_spec trustzone]]} {
+ puts "Run script does not support this platform."
+ exit 0
+}
+
+set build_components {
+ app/ssh_client
+ drivers/nic
+ lib/vfs/import
+ lib/vfs/lwip
+}
+
+proc gpio_drv { } { if {[have_spec rpi] && [have_spec hw]} { return hw_gpio_drv }
+ if {[have_spec rpi] && [have_spec foc]} { return foc_gpio_drv }
+ return gpio_drv }
+
+lappend_if [have_spec gpio] build_components drivers/gpio
+
+source ${genode_dir}/repos/base/run/platform_drv.inc
+append_platform_drv_build_components
+
+lappend_if [expr {[nic_drv_binary] == "nic_drv"}] build_components drivers/nic
+lappend_if [expr {[nic_drv_binary] == "usb_drv"}] build_components drivers/usb
+
+build $build_components
+
+create_boot_directory
+
+import_from_depot \
+ genodelabs/src/[base_src] \
+ genodelabs/pkg/[drivers_interactive_pkg] \
+ genodelabs/src/init \
+ genodelabs/pkg/terminal \
+ genodelabs/src/input_filter \
+
+append config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+append_platform_drv_config
+
+append_if [have_spec gpio] config "
+
+
+
+
+ "
+
+append config {
+
+
+
+
+
+
+
+
+ } [nic_drv_config] {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2000-01-01 00:00
+ 01234567890123456789
+
+
+
+
+
+
+
+
+
+
+
+
+}
+
+install_config $config
+
+# generic modules
+set boot_modules {
+ libc.lib.so
+ libcrypto.lib.so
+ libm.lib.so
+ libssh.lib.so
+ ssh_client
+ vfs_import.lib.so
+ vfs.lib.so
+ vfs_lwip.lib.so
+ zlib.lib.so
+}
+
+# platform-specific modules
+append_platform_drv_boot_modules
+
+lappend boot_modules [nic_drv_binary]
+
+lappend_if [have_spec ps2] boot_modules ps2_drv
+lappend_if [have_spec framebuffer] boot_modules fb_drv
+
+lappend_if [have_spec gpio] boot_modules [gpio_drv]
+
+build_boot_image $boot_modules
+
+append_if [have_spec x86] qemu_args " -net nic,model=e1000 "
+append_if [have_spec lan9118] qemu_args " -net nic,model=lan9118 "
+append qemu_args " -net user -net dump,file=[run_dir].pcap"
+
+run_genode_until forever
diff --git a/src/app/ssh_client/component.cc b/src/app/ssh_client/component.cc
new file mode 100644
index 0000000..076d02c
--- /dev/null
+++ b/src/app/ssh_client/component.cc
@@ -0,0 +1,330 @@
+/*
+ * \brief SSH client as a Terminal client
+ * \author Prashanth Mundkur
+ * \author Emery Hemingway
+ * \date 2018-06-18
+ */
+
+/*
+ * Copyright (C) 2018 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU Affero General Public License version 3.
+ */
+
+/* Genode includes */
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* Libssh includes */
+#include
+#include
+
+/* Libc includes */
+#include
+#include
+
+
+namespace Ssh_client {
+ using namespace Genode;
+ struct Main;
+
+ typedef Genode::String<64> String;
+
+ String string_attr(Xml_node const &node,
+ char const *key, char const *def = "") {
+ return node.attribute_value(key, String(def)); }
+}
+
+
+struct Ssh_client::Main
+{
+ Libc::Env &_env;
+
+ Terminal::Connection _terminal { _env };
+
+ Genode::Signal_handler _terminal_handler {
+ _env.ep(), *this, &Main::_handle_terminal };
+
+ Genode::Signal_handler _size_handler {
+ _env.ep(), *this, &Main::_handle_size };
+
+ Libc::Select_handler _select_handler {
+ *this, &Main::_select_ready };
+
+ ssh_session _session = ssh_new();
+ ssh_channel _channel = NULL;
+
+ typedef Genode::String<128> String;
+ String _hostname { };
+ String _password { };
+
+ /* must the host be known */
+ bool _host_known = true;
+
+ void _exit(int code)
+ {
+ if (_session) {
+ if (_channel) {
+ ssh_channel_free(_channel);
+ }
+ ssh_free(_session);
+ }
+
+ ssh_finalize();
+ _env.parent().exit(code);
+ Genode::sleep_forever();
+ }
+
+ void _die()
+ {
+ if (_session)
+ Genode::error(ssh_get_error(_session));
+ _exit(~0);
+ }
+
+ void _handle_terminal()
+ {
+ Libc::with_libc([&] () {
+ while (_terminal.avail()) {
+ char buf[256];
+ size_t n = _terminal.read(buf, sizeof(buf));
+ ssh_channel_write(_channel, buf, n);
+ }
+ });
+ }
+
+ void _handle_size()
+ {
+ Libc::with_libc([&] () {
+ auto size = _terminal.size();
+ ssh_channel_change_pty_size(_channel, size.columns(), size.lines());
+ });
+ }
+
+ void _handle_channel(int nready)
+ {
+ Libc::with_libc([&] () {
+ char buffer[256];
+
+ fd_set readfds;
+ fd_set noop;
+
+ if (ssh_channel_is_eof(_channel)) _exit(0);
+
+ while (nready) {
+ while (true) {
+ int n = ssh_channel_read_nonblocking(_channel, buffer, sizeof(buffer), 0);
+ if (!n) break;
+ if (n < 0) _die();
+ _terminal.write(buffer, n);
+ }
+
+ FD_ZERO(&noop);
+ FD_ZERO(&readfds);
+ FD_SET(ssh_get_fd(_session), &readfds);
+
+ nready = _select_handler.select(ssh_get_fd(_session)+1, readfds, noop, noop);
+ }
+ });
+ }
+
+ void _select_ready(int nready, fd_set const &readfds, fd_set const &writefds, fd_set const &exceptfds)
+ {
+ _handle_channel(nready);
+ }
+
+ static void _log_host_usage()
+ {
+ char buf[1024];
+ Xml_generator gen(buf, sizeof(buf), "host", [&gen] () {
+ gen.attribute("name", "...");
+ gen.attribute("port", 22);
+ gen.attribute("user", "...");
+ gen.attribute("pass", "...");
+ gen.attribute("known", "yes");
+ });
+
+ log("host file format: ", Xml_node(buf, gen.used()));
+ }
+
+ void _configure()
+ {
+ using namespace Genode;
+
+ _env.config([&] (Xml_node const &config) {
+ int verbosity = config.attribute_value("verbose", false)
+ ? SSH_LOG_FUNCTIONS : SSH_LOG_NOLOG;
+ ssh_options_set(_session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);
+ });
+
+ /* read all files from the root directory */
+ ssh_options_set(_session, SSH_OPTIONS_SSH_DIR, "/");
+ ssh_options_set(_session, SSH_OPTIONS_KNOWNHOSTS, "/known_hosts");
+
+ /* read the XML formatted host file */
+ char buf[4096];
+ FILE *f = fopen("host", "r");
+ if (f == NULL) {
+ error("failed to open \"/host\" configuration file");
+ _log_host_usage();
+ _exit(~0);
+ }
+ size_t n = fread(buf, 1, sizeof(buf), f);
+ fclose(f);
+
+ Xml_node host_cfg(buf, n);
+ try {
+ host_cfg.attribute("name").value(&_hostname);
+ ssh_options_set(_session, SSH_OPTIONS_HOST,
+ _hostname.string());
+ ssh_options_set(_session, SSH_OPTIONS_PORT_STR,
+ string_attr(host_cfg, "port").string());
+ ssh_options_set(_session, SSH_OPTIONS_USER,
+ string_attr(host_cfg, "user").string());
+ _password = host_cfg.attribute_value("pass", String());
+ _host_known = host_cfg.attribute_value("known", true);
+ }
+ catch (...) {
+ error("failed to parse host configuration");
+ error(host_cfg);
+ _log_host_usage();
+ throw;
+ _exit(~0);
+ }
+ }
+
+ void _authenticate_public_key()
+ {
+ int rc = SSH_AUTH_AGAIN;
+ while (rc == SSH_AUTH_AGAIN) {
+ rc = ssh_userauth_publickey_auto(_session, NULL, NULL);
+ switch (rc) {
+ case SSH_AUTH_SUCCESS:
+ Genode::log("public key authentication successful"); return;
+ case SSH_AUTH_ERROR:
+ Genode::error("public key authentication failed"); break;
+ case SSH_AUTH_DENIED:
+ Genode::error("public key authentication denied"); break;
+ case SSH_AUTH_PARTIAL:
+ Genode::error("additional authentication is required"); break;
+ case SSH_AUTH_AGAIN:
+ default:
+ break;
+ }
+ }
+
+ if (ssh_userauth_password(_session, NULL, _password.string()) == SSH_OK) {
+ Genode::log("password authentication successful");
+ return;
+ }
+
+ Genode::error("password authentication denied");
+ if (ssh_userauth_none(_session, NULL) == SSH_OK) {
+ Genode::log("anonymous authentication successful");
+ }
+
+ _die();
+ }
+
+ void _connect()
+ {
+ if (ssh_connect(_session) != SSH_OK) _die();
+
+ {
+ ssh_key hostkey = NULL;
+ ssh_get_publickey(_session, &hostkey);
+ {
+ unsigned char *hash = NULL;
+ size_t hashlen = 0;
+ ssh_get_publickey_hash(hostkey, SSH_PUBLICKEY_HASH_SHA1,
+ &hash, &hashlen);
+ {
+ char *hexhost = ssh_get_hexa(hash, hashlen);
+ log(_hostname, " ", (char const *)hexhost);
+ ssh_string_free_char(hexhost);
+ }
+ ssh_clean_pubkey_hash(&hash);
+ }
+ ssh_key_free(hostkey);
+ }
+
+ if (!ssh_is_server_known(_session)) {
+ if (_host_known) {
+ error("unknown host");
+ _exit(~0);
+ } else {
+ ssh_write_knownhost(_session);
+ }
+ }
+
+ _authenticate_public_key();
+
+ if (char *banner = ssh_get_issue_banner(_session)) {
+ Genode::log((const char *)banner);
+ free(banner);
+ }
+
+ _channel = ssh_channel_new(_session);
+ if (_channel == NULL) _die();
+
+ if (ssh_channel_open_session(_channel) != SSH_OK) _die();
+
+ auto size = _terminal.size();
+ if (ssh_channel_request_pty_size(_channel, "screen",
+ size.columns(),
+ size.lines()) != SSH_OK) _die();
+
+ if (ssh_channel_request_shell(_channel) != SSH_OK) _die();
+ }
+
+ Main(Libc::Env &env) : _env(env)
+ {
+ if (!_session) {
+ Genode::error("failed to initialize libssh session");
+ _die();
+ }
+
+ _terminal.read_avail_sigh(_terminal_handler);
+ _terminal.size_changed_sigh(_size_handler);
+
+ _configure();
+ _connect();
+
+ _handle_channel(1);
+ _handle_terminal();
+ }
+
+ ~Main()
+ {
+ ssh_free(_session);
+ }
+};
+
+
+static void log_callback(int priority,
+ const char *function,
+ const char *buffer,
+ void *userdata)
+{
+ (void)userdata;
+ (void)function;
+ Genode::log(buffer);
+}
+
+
+void Libc::Component::construct(Libc::Env &env)
+{
+ with_libc([&] () {
+ Genode::log("libssh ", ssh_version(0));
+
+ ssh_set_log_callback(log_callback);
+ ssh_init();
+
+ static Ssh_client::Main main(env);
+ });
+}
diff --git a/src/app/ssh_client/target.mk b/src/app/ssh_client/target.mk
new file mode 100644
index 0000000..b1c2ff9
--- /dev/null
+++ b/src/app/ssh_client/target.mk
@@ -0,0 +1,5 @@
+TARGET := ssh_client
+LIBS += base libc libssh
+SRC_CC += component.cc
+
+CC_CXX_WARN_STRICT =