SSH terminal client

A client of the Terminal service 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:
<host name="sdf-eu.org" port="22" user="root" pass="foo" known="yes"/>

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 the negative in the host file.
This commit is contained in:
Emery Hemingway
2018-07-16 19:28:52 +02:00
committed by Norman Feske
parent 2ba33d38a9
commit 5fff9d4b5b
10 changed files with 628 additions and 0 deletions

View File

@@ -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:
<host name="sdf-eu.org" port="22" user="root" pass="foo" known="yes"/>
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.

View File

@@ -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

View File

@@ -0,0 +1 @@
2018-07-17-k fb159d9fe48bb3081f79f0d78e4b41dba0a164a4

View File

@@ -0,0 +1,82 @@
<runtime ram="32M" caps="512" binary="init">
<requires>
<file_system/>
<nic/>
<nitpicker/>
<timer/>
</requires>
<content>
<rom label="init"/>
<rom label="ld.lib.so"/>
<rom label="libc.lib.so"/>
<rom label="libcrypto.lib.so"/>
<rom label="libm.lib.so"/>
<rom label="libssh.lib.so"/>
<rom label="nit_fb"/>
<rom label="ssh_client"/>
<rom label="terminal"/>
<rom label="vfs.lib.so"/>
<rom label="vfs_jitterentropy.lib.so"/>
<rom label="vfs_lwip.lib.so"/>
<rom label="zlib.lib.so"/>
</content>
<config>
<parent-provides>
<service name="ROM"/>
<service name="PD"/>
<service name="RM"/>
<service name="CPU"/>
<service name="LOG"/>
<service name="Timer"/>
<service name="File_system"/>
<service name="Nic"/>
<service name="Nitpicker"/>
</parent-provides>
<default-route> <any-service> <parent/> <any-child/> </any-service> </default-route>
<default caps="100"/>
<start name="nit_fb">
<resource name="RAM" quantum="4M"/>
<provides> <service name="Framebuffer"/> <service name="Input"/> </provides>
<config xpos="10" ypos="10" initial_width="800" initial_height="600"/>
</start>
<start name="terminal">
<resource name="RAM" quantum="4M"/>
<provides> <service name="Terminal"/> </provides>
<config>
<vfs> <dir name="fonts"> <fs/> </dir> </vfs>
</config>
<route>
<service name="File_system"> <parent label="fonts"/> </service>
<any-service> <parent/> <any-child/> </any-service>
</route>
</start>
<start name="ssh_client" caps="256">
<resource name="RAM" quantum="32M" />
<exit propagate="yes"/>
<config>
<vfs>
<fs/>
<dir name="dev">
<log/>
<jitterentropy name="random"/>
</dir>
<dir name="socket"> <lwip dhcp="yes"/> </dir>
</vfs>
<libc stdout="/dev/log" stderr="/dev/log" socket="/socket"/>
</config>
<route>
<service name="File_system"> <parent label="ssh"/> </service>
<any-service> <parent/> <any-child/> </any-service>
</route>
</start>
</config>
</runtime>

View File

@@ -0,0 +1,2 @@
SRC_DIR = src/app/ssh_client
include $(GENODE_DIR)/repos/base/recipes/src/content.inc

View File

@@ -0,0 +1 @@
2018-07-17 54242fdfe92139b894d789ecb438f632d1214ff9

View File

@@ -0,0 +1,6 @@
base
libc
libssh
os
terminal_session
vfs

177
run/ssh_client.run Normal file
View File

@@ -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 {
<config>
<parent-provides>
<service name="CPU"/>
<service name="IO_MEM"/>
<service name="IO_PORT"/>
<service name="IRQ"/>
<service name="LOG"/>
<service name="PD"/>
<service name="RAM"/>
<service name="RM"/>
<service name="ROM"/>
</parent-provides>
<default caps="100"/>
<default-route>
<any-service> <parent/> <any-child/> </any-service>
</default-route>}
append_platform_drv_config
append_if [have_spec gpio] config "
<start name=\"[gpio_drv]\" caps=\"140\">
<resource name=\"RAM\" quantum=\"4M\"/>
<provides><service name=\"Gpio\"/></provides>
<config/>
</start>"
append config {
<start name="timer">
<resource name="RAM" quantum="1M"/>
<provides> <service name="Timer"/> </provides>
</start>
<start name="nic_drv" caps="120">
<binary name="} [nic_drv_binary] {"/>
<resource name="RAM" quantum="16M"/>
<provides> <service name="Nic"/> </provides>
} [nic_drv_config] {
</start>
<start name="fb_drv" caps="200">
<resource name="RAM" quantum="10M"/>
<provides><service name="Framebuffer"/></provides>
<config buffered="yes"/>
</start>
<start name="ps2_drv">
<resource name="RAM" quantum="1M"/>
<provides><service name="Input"/></provides>
</start>
<start name="input_filter" caps="80">
<resource name="RAM" quantum="1M"/>
<provides> <service name="Input"/> </provides>
<config>
<input label="ps2"/>
<output>
<chargen>
<input name="ps2"/>
<repeat delay_ms="230" rate_ms="90"/>
<include rom="en_us.chargen"/>
<include rom="special.chargen"/>
</chargen>
</output>
</config>
<route>
<service name="Input" label="ps2"> <child name="ps2_drv"/> </service>
<any-service> <parent/> <any-child/> </any-service>
</route>
</start>
<start name="terminal">
<resource name="RAM" quantum="2M"/>
<provides><service name="Terminal"/></provides>
<route>
<service name="ROM" label="config">
<parent label="terminal.config"/> </service>
<service name="Input">
<child name="input_filter"/> </service>
<any-service> <parent/> <any-child/> </any-service>
</route>
</start>
<start name="ssh_client" caps="500">
<resource name="RAM" quantum="32M"/>
<config>
<vfs>
<ram/>
<dir name="dev">
<log/> <null/> <inline name="rtc">2000-01-01 00:00</inline>
<inline name="random">01234567890123456789</inline>
</dir>
<dir name="socket"> <lwip dhcp="yes"/> </dir>
<import>
<!-- sdf-eu.org -->
<inline name="host"><host name="178.63.35.194" user="new" known="no"/></inline>
</import>
</vfs>
<libc stdout="/dev/log" stderr="/dev/log" rtc="/dev/rtc" socket="/socket"/>
</config>
</start>
</config>
}
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

View File

@@ -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 <util/reconstructible.h>
#include <libc/select.h>
#include <terminal_session/connection.h>
#include <libc/component.h>
#include <util/xml_generator.h>
#include <base/sleep.h>
/* Libssh includes */
#include <libssh/libssh.h>
#include <libssh/callbacks.h>
/* Libc includes */
#include <stdio.h>
#include <stdlib.h>
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<Main> _terminal_handler {
_env.ep(), *this, &Main::_handle_terminal };
Genode::Signal_handler<Main> _size_handler {
_env.ep(), *this, &Main::_handle_size };
Libc::Select_handler<Main> _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);
});
}

View File

@@ -0,0 +1,5 @@
TARGET := ssh_client
LIBS += base libc libssh
SRC_CC += component.cc
CC_CXX_WARN_STRICT =