ssh: add exec channel and exit on interactive

This commit implements the ssh exec channel request. It also handles
some shortcommings on the interactive channel like exit and concurrent
session establishments.

Pipes into the channel do not work yet. E.g.:
echo foobar | ssh noux@localhost -p 5555 "cat > /rw/test.txt"

The issue described with FIXME in Ssh::Server::incoming_connection()
could not be reproduced and might have been fixed with the improved
file descriptor handling.

Fixes #3401
This commit is contained in:
Sid Hussmann 2019-05-29 18:44:28 +02:00 committed by Christian Helmuth
parent 92bdcbf1fe
commit eaefcc2c6f
18 changed files with 2465 additions and 1525 deletions

View File

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

View File

@ -0,0 +1 @@
2019-06-04 819552cf88bb31124101557a618f4d00de8ed8b2

View File

@ -0,0 +1,3 @@
base
os
report_session

View File

@ -0,0 +1,320 @@
#
# \brief Test for the SSH terminal
#
assert_spec x86
if {[have_spec linux]} {
puts "Run script is not supported on this platform."
exit 0
}
# Build
#
source ${genode_dir}/repos/base/run/platform_drv.inc
append_platform_drv_build_components
build $build_components
create_boot_directory
import_from_depot [depot_user]/src/[base_src]
import_from_depot [depot_user]/src/bash
import_from_depot [depot_user]/src/coreutils-minimal
import_from_depot [depot_user]/src/exec_terminal
import_from_depot [depot_user]/src/init
import_from_depot [depot_user]/src/ipxe_nic_drv
import_from_depot [depot_user]/src/libc
import_from_depot [depot_user]/src/libcrypto
import_from_depot [depot_user]/src/libssh
import_from_depot [depot_user]/src/noux
import_from_depot [depot_user]/src/platform_drv
import_from_depot [depot_user]/src/posix
import_from_depot [depot_user]/src/ram_fs
import_from_depot [depot_user]/src/rtc_drv
import_from_depot [depot_user]/src/ssh_terminal
import_from_depot [depot_user]/src/vfs
import_from_depot [depot_user]/src/vfs_jitterentropy
import_from_depot [depot_user]/src/vfs_lxip
import_from_depot [depot_user]/src/vim-minimal
import_from_depot [depot_user]/src/zlib
#
# Generate config
#
set config {
<config verbose="no">
<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"/>
<start name="timer">
<resource name="RAM" quantum="1M"/>
<provides> <service name="Timer"/> </provides>
<route>
<any-service> <parent/> <any-child/> </any-service>
</route>
</start>
<start name="nic_drv">
<binary name="ipxe_nic_drv"/>
<resource name="RAM" quantum="4M"/>
<provides> <service name="Nic"/> </provides>
<route>
<any-service> <parent/> <any-child/> </any-service>
</route>
</start>
<start name="rtc_drv">
<resource name="RAM" quantum="1M"/>
<provides> <service name="Rtc"/> </provides>
<route>
<any-service> <parent/> <any-child/> </any-service>
</route>
</start>
<start name="ram_fs">
<resource name="RAM" quantum="8M"/>
<provides> <service name="File_system"/> </provides>
<config>
<default-policy root="/" writeable="yes"/>
</config>
<route>
<any-service> <parent/> <any-child/> </any-service>
</route>
</start>
<start name="ssh_terminal" caps="250">
<resource name="RAM" quantum="32M"/>
<provides> <service name="Terminal"/> </provides>
<config port="22" allow_password="yes" show_password="yes" ed25519_key="/etc/ssh/ed25519_key">
<policy label="dynamic -> noux -> " user="noux" password="xuon" multi_login="yes" request_terminal="yes"/>
<policy label_prefix="always-running-noux" user="charlie" password="xuon"/>
<libc stdout="/dev/log" stderr="/dev/log" socket="/socket" rtc="/dev/rtc"/>
<vfs>
<dir name="dev">
<log/>
<jitterentropy name="random"/>
<jitterentropy name="urandom"/>
<inline name="rtc">2000-01-01 00:00</inline>
</dir>
<dir name="etc">
<dir name="ssh">
<rom name="ed25519_key"/>
</dir>
</dir>
<dir name="socket"> <lxip dhcp="yes"/> </dir>
</vfs>
</config>
<route>
<service name="Nic"> <child name="nic_drv"/> </service>
<service name="Report"> <child name="report_rom"/> </service>
<service name="Timer"> <child name="timer"/> </service>
<service name="CPU"> <parent/> </service>
<service name="LOG"> <parent/> </service>
<service name="PD"> <parent/> </service>
<service name="RM"> <parent/> </service>
<service name="ROM"> <parent/> </service>
</route>
</start>
<start name="report_rom" caps="100">
<resource name="RAM" quantum="4M"/>
<provides>
<service name="Report"/>
<service name="ROM"/>
</provides>
<config>
<policy label="exec_terminal -> exec_terminal.config" report="ssh_terminal -> request_terminal"/>
<policy label="dynamic -> config" report="exec_terminal -> config"/>
</config>
<route>
<service name="CPU"> <parent/> </service>
<service name="LOG"> <parent/> </service>
<service name="PD"> <parent/> </service>
<service name="RM"> <parent/> </service>
<service name="ROM"> <parent/> </service>
</route>
</start>
<start name="exec_terminal" caps="100">
<resource name="RAM" quantum="4M"/>
<route>
<service name="ROM" label="exec_terminal.config"> <child name="report_rom"/> </service>
<service name="Report" label="config"> <child name="report_rom"/> </service>
<service name="Timer"> <child name="timer"/> </service>
<service name="CPU"> <parent/> </service>
<service name="LOG"> <parent/> </service>
<service name="PD"> <parent/> </service>
<service name="RM"> <parent/> </service>
<service name="ROM"> <parent/> </service>
</route>
</start>
<start name="dynamic" caps="1000">
<binary name="init"/>
<resource name="RAM" quantum="32M"/>
<route>
<service name="File_system"> <child name="ram_fs"/> </service>
<service name="ROM" label="config"> <child name="report_rom"/> </service>
<service name="ROM" label_last="coreutils-minimal.tar"> <parent label="coreutils-minimal.tar"/> </service>
<service name="ROM" label_last="vim-minimal.tar"> <parent label="vim-minimal.tar"/> </service>
<service name="Terminal"> <child name="ssh_terminal"/> </service>
<service name="Timer"> <child name="timer"/> </service>
<service name="CPU"> <parent/> </service>
<service name="LOG"> <parent/> </service>
<service name="PD"> <parent/> </service>
<service name="RM"> <parent/> </service>
<service name="ROM"> <parent/> </service>
</route>
</start>
<start name="always-running-noux" caps="500">
<binary name="noux"/>
<resource name="RAM" quantum="64M"/>
<config>
<fstab>
<tar name="bash.tar" />
<tar name="coreutils-minimal.tar" />
<tar name="vim-minimal.tar" />
</fstab>
<start name="/bin/bash">
<env name="TERM" value="screen"/>
<env name="HOME" value="/"/>
<env name="IGNOREEOF" value="3"/>
</start>
</config>
<route>
<service name="File_system"> <child name="ram_fs"/> </service>
<service name="ROM" label="config"> <child name="report_rom"/> </service>
<service name="ROM" label_last="coreutils-minimal.tar"> <parent label="coreutils-minimal.tar"/> </service>
<service name="ROM" label_last="vim-minimal.tar"> <parent label="vim-minimal.tar"/> </service>
<service name="Terminal"> <child name="ssh_terminal"/> </service>
<service name="Timer"> <child name="timer"/> </service>
<service name="CPU"> <parent/> </service>
<service name="LOG"> <parent/> </service>
<service name="PD"> <parent/> </service>
<service name="RM"> <parent/> </service>
<service name="ROM"> <parent/> </service>
</route>
</start>
}
append_platform_drv_config
append config {
</config>}
install_config $config
#
# Generate a new host key
#
if {![file exists bin/ed25519_key]} {
exec ssh-keygen -t ed25519 -f bin/ed25519_key -q -N ""
}
#
# Boot modules
#
# generic modules
set boot_modules {
ed25519_key
}
# platform-specific modules
append_platform_drv_boot_modules
build_boot_image $boot_modules
#
# Execute test
#
append qemu_args " -m 512 -nographic "
proc qemu_nic_model {} {
if [have_spec x86] { return e1000 }
if [have_spec lan9118] { return lan9118 }
return nic_model_missing
}
append qemu_args " -netdev user,id=net0,hostfwd=tcp::5555-:22 "
append qemu_args " -net nic,model=[qemu_nic_model],netdev=net0 "
set lxip_match_string "ipaddr=(\[0-9\]+\.\[0-9\]+\.\[0-9\]+\.\[0-9\]+).*\n"
if {[get_cmd_switch --autopilot]} {
run_genode_until $lxip_match_string 60
set serial_id [output_spawn_id]
if {[have_include "power_on/qemu"]} {
set host "localhost"
set port "5555"
} else {
regexp $lxip_match_string $output all host
puts ""
set port "22"
}
# wait for ssh_terminal to come up
run_genode_until "--- SSH terminal started ---" 15 $serial_id
for {set index 0} {$index < 10} {incr index} {
puts "test interactive channel"
spawn sshpass -p xuon ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -l noux $host -p $port
set ssh_id $spawn_id
run_genode_until "--- noux started ---" 15 $serial_id
send -i $ssh_id "ls\r"
run_genode_until "bin" 5 $ssh_id
send -i $ssh_id "exit\r"
run_genode_until "child \"noux\" exited with exit value 0" 15 $serial_id
puts "test exec channel echo"
set echo_text "The quick brown fox jumps over the lazy dog"
spawn sshpass -p xuon ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -l noux $host -p $port "echo $echo_text"
set ssh_id $spawn_id
run_genode_until "--- noux started ---" 15 $serial_id
run_genode_until $echo_text 5 $ssh_id
run_genode_until "child \"noux\" exited with exit value 0" 15 $serial_id
puts "test exec channel ls"
spawn sshpass -p xuon ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -l noux $host -p $port "ls"
set ssh_id $spawn_id
run_genode_until "--- noux started ---" 15 $serial_id
run_genode_until "bin" 5 $ssh_id
run_genode_until "child \"noux\" exited with exit value 0" 15 $serial_id
puts "test exec channel with empty command will not hang"
spawn sshpass -p xuon ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -l noux $host -p $port " "
set ssh_id $spawn_id
run_genode_until "--- noux started ---" 15 $serial_id
run_genode_until "child \"noux\" exited with exit value" 15 $serial_id
}
puts ""
puts ""
} else {
run_genode_until forever
}
exec rm bin/ed25519_key bin/ed25519_key.pub
# vi: set ft=tcl :

View File

@ -4,7 +4,7 @@ Before using the component, please consult the _Notes_ section to learn
about the current subtleties.
For an example on how to use the server please look at the run script
provided by _repos/gem/run/ssh_terminal_.
provided by _repos/gems/run/ssh_terminal_ or _repos/gems/run/ssh_exec_channel_.
Configuration
@ -52,14 +52,14 @@ Aside from the mandatory attributes there are optional ones:
be printed to the LOG session. The default is 'yes'.
The relation between a Terminal session and a SSH session is established
by a '<policy>' node. It first specifes which Terminal client may access
by a '<policy>' node. It first specifies which Terminal client may access
the server. Second and more importantly it specifies which SSH session has
access to which Terminal session. The non policy-label specific attributes
of the node form a login that is used to authenticate access via the SSH
session:
* 'user' sets the name of a login and is used throughout the component
to establish the relation between Terminal and SSH session and is therefor
to establish the relation between Terminal and SSH session and is therefore
mandatory.
* 'password' sets the password of the login, which is used to authenticate
@ -75,7 +75,7 @@ Either one of the 'password' or 'pub_key' attributes must be set in order for
the login to be valid.
Normally, all Terminal sessions are expected to be established before an SSH
connection is made. To accommdate the use-case where the Terminal client is
connection is made. To accommodate the use-case where the Terminal client is
made available in a dynamic fashion, the 'request_terminal' attribute can by
set to 'yes'. In this case the SSH Terminal server will generate a report,
which then can be inspected and reacted upon by a management component,
@ -91,24 +91,31 @@ The following snippet shows such a report:
The component will generate the report only once when the first login
via SSH is made and the corresponding Terminal session is not available.
All subsequent logins have to wait until the session is provided. If
there are active SSH session after the Terminal in question has been detached,
a new report will be generated.
All subsequent logins have to wait until the session is provided. Once the
Terminal session has been detached an 'exit' report is generated. Detaching a
terminal client will lead to a disconnect for the corresponding SSH session If
there are active SSH session after the Terminal in question has been detached, a
new report will be generated.
Notes
~~~~~
* The SSH connection MUST be forcefully cut at the client, e.g. when
using OpenSSH by entering '~.'.
* A helper component is needed, in case an exec channel is required or when
Terminal sessions need to be started *after* the SSH connection is
established. An example can be found here: _repos/gems/src/test/exec_terminal/
* Using tools that require an exec channel, e.g. scp(1) and rsync(1), is not
supported.
* The SSH connection MUST be forcefully cut at the client in case the Terminal
session is established *before* the ssh channel is open. This can be done
when using OpenSSH by entering '~.'.
* Host keys can be generated by using ssh-keygen(1) on your host system
(e.g., 'ssh-keygen -t ed25519_key -f ed25519_key' without a passphrase and
then use the 'ed25519_key' file).
* Reports from concurrent logins will override each other and potentially lead
to lost reports.
* Although concurrent access to one Terminal session via multiple SSH sessions
at the same time is possible, it should better be avoided as there are no
safety measures in place.

View File

@ -0,0 +1,189 @@
/*
* \brief Component providing a Terminal session via SSH
* \author Josef Soentgen
* \author Pirmin Duss
* \date 2019-05-29
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
* Copyright (C) 2019 gapfruit AG
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
#ifndef _SSH_TERMINAL_LOGIN_H_
#define _SSH_TERMINAL_LOGIN_H_
/* Genode includes */
#include <util/string.h>
#include <base/heap.h>
/* libssh includes */
#include <libssh/libssh.h>
/* local includes */
#include "util.h"
namespace Ssh {
using namespace Genode;
using namespace Util;
using User = String<32>;
using Password = String<64>;
using Hash = String<65>;
struct Login;
struct Login_registry;
}
struct Ssh::Login : Genode::Registry<Ssh::Login>::Element
{
Ssh::User user { };
Ssh::Password password { };
Ssh::Hash pub_key_hash { };
ssh_key pub_key { nullptr };
bool multi_login { false };
bool request_terminal { false };
/**
* Constructor
*/
Login(Genode::Registry<Login> &reg,
Ssh::User const &user,
Ssh::Password const &pw,
Filename const &pk_file,
bool const multi_login,
bool const request_terminal)
:
Element(reg, *this),
user(user), password(pw), multi_login(multi_login),
request_terminal(request_terminal)
{
Libc::with_libc([&] {
if (pk_file.valid() &&
ssh_pki_import_pubkey_file(pk_file.string(), &pub_key)) {
Genode::error("could not import public key for user '",
user, "'");
}
if (pub_key) {
unsigned char *h = nullptr;
size_t hlen = 0;
/* pray and assume both calls never fail */
ssh_get_publickey_hash(pub_key, SSH_PUBLICKEY_HASH_SHA256,
&h, &hlen);
char const *p = ssh_get_fingerprint_hash(SSH_PUBLICKEY_HASH_SHA256,
h, hlen);
if (p) {
pub_key_hash = Ssh::Hash(p);
}
ssh_clean_pubkey_hash(&h);
/* abuse function to free fingerprint */
ssh_clean_pubkey_hash((unsigned char**)&p);
}
}); /* Libc::with_libc */
}
virtual ~Login() { ssh_key_free(pub_key); }
bool auth_password() const { return password.valid(); }
bool auth_publickey() const { return pub_key != nullptr; }
void print(Genode::Output &out) const
{
Genode::print(out, "user ", user, ": ");
if (auth_password()) { Genode::print(out, "password "); }
if (auth_publickey()) { Genode::print(out, "public-key"); }
}
};
struct Ssh::Login_registry : Genode::Registry<Ssh::Login>
{
Genode::Allocator &_alloc;
Genode::Lock _lock { };
/**
* Import one login from node
*/
bool _import_single(Genode::Xml_node const &node)
{
User user = node.attribute_value("user", User());
Password pw = node.attribute_value("password", Password());
Filename pub = node.attribute_value("pub_key", Filename());
bool multi_login = node.attribute_value("multi_login", false);
bool req_term = node.attribute_value("request_terminal", false);
if (!user.valid() || (!pw.valid() && !pub.valid())) {
Genode::warning("ignoring invalid policy");
return false;
}
if (lookup(user.string())) {
Genode::warning("ignoring already imported login ", user.string());
return false;
}
try {
new (&_alloc) Login(*this, user, pw, pub, multi_login, req_term);
return true;
} catch (...) { return false; }
}
void _remove_all()
{
for_each([&] (Login &login) {
Genode::destroy(&_alloc, &login);
});
}
/**
* Constructor
*
* \param alloc allocator for registry elements
*/
Login_registry(Genode::Allocator &alloc) : _alloc(alloc) { }
/**
* Return registry lock
*/
Genode::Lock &lock() { return _lock; }
/**
* Import all login information from config
*/
void import(Genode::Xml_node const &node)
{
_remove_all();
try {
node.for_each_sub_node("policy",
[&] (Genode::Xml_node const &node) {
_import_single(node);
});
} catch (...) { }
}
/**
* Look up login information by user name
*/
Ssh::Login const *lookup(char const *user) const
{
Login const *p = nullptr;
auto lookup_user = [&] (Login const &login) {
if (login.user == user) { p = &login; }
};
for_each(lookup_user);
return p;
}
};
#endif /* _SSH_TERMINAL_LOGIN_H_ */

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,113 @@
/*
* \brief Component providing a Terminal session via SSH
* \author Josef Soentgen
* \author Pirmin Duss
* \date 2019-05-29
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
* Copyright (C) 2019 gapfruit AG
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
#ifndef _SSH_TERMINAL_ROOT_COMPONENT_H_
#define _SSH_TERMINAL_ROOT_COMPONENT_H_
/* Genode includes */
#include <root/component.h>
#include <os/session_policy.h>
/* local includes */
#include "session_component.h"
#include "server.h"
namespace Terminal
{
using namespace Genode;
class Root_component;
}
class Terminal::Root_component : public Genode::Root_component<Session_component>
{
private:
Genode::Env &_env;
Genode::Attached_rom_dataspace _config_rom { _env, "config" };
Genode::Xml_node _config { _config_rom.xml() };
Genode::Heap _logins_alloc { _env.ram(), _env.rm() };
Ssh::Login_registry _logins { _logins_alloc };
Ssh::Server _server { _env, _config, _logins };
Genode::Signal_handler<Terminal::Root_component> _config_sigh {
_env.ep(), *this, &Terminal::Root_component::_handle_config_update };
void _handle_config_update()
{
_config_rom.update();
if (!_config_rom.valid()) { return; }
{
Genode::Lock::Guard g(_logins.lock());
_logins.import(_config_rom.xml());
}
_server.update_config(_config_rom.xml());
}
protected:
Session_component *_create_session(const char *args)
{
try {
Session_label const label = label_from_args(args);
Session_policy policy(label, _config);
Ssh::User const user = policy.attribute_value("user", Ssh::User());
if (!user.valid()) { throw -1; }
Ssh::Login const *login = _logins.lookup(user.string());
if (!login) { throw -1; }
Session_component *s = nullptr;
s = new (md_alloc()) Session_component(_env, 4096, login->user);
try {
Libc::with_libc([&] () { _server.attach_terminal(*s); });
return s;
} catch (...) {
Genode::destroy(md_alloc(), s);
throw;
}
} catch (...) { throw Service_denied(); }
}
void _destroy_session(Session_component *s)
{
_server.detach_terminal(*s);
Genode::destroy(md_alloc(), s);
}
public:
Root_component(Genode::Env &env,
Genode::Allocator &md_alloc)
:
Genode::Root_component<Session_component>(&env.ep().rpc_ep(),
&md_alloc),
_env(env)
{
_config_rom.sigh(_config_sigh);
_handle_config_update();
}
};
#endif /* _SSH_TERMINAL_ROOT_COMPONENT_H_ */

View File

@ -0,0 +1,607 @@
/*
* \brief Component providing a Terminal session via SSH
* \author Josef Soentgen
* \author Pirmin Duss
* \author Sid Hussmann
* \date 2019-05-29
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
* Copyright (C) 2019 gapfruit AG
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
/* local includes */
#include "server.h"
/*
* Add the libssh callback forward declarations here so that we can use them
* from within the classes and thereby document the ones currently implemented.
*/
extern int channel_data_cb(ssh_session, ssh_channel, void *, uint32_t, int, void *);
extern int channel_env_request_cb(ssh_session, ssh_channel, char const *, char const *, void *);
extern int channel_pty_request_cb(ssh_session, ssh_channel, char const *, int, int, int, int, void *);
extern int channel_pty_window_change_cb(ssh_session, ssh_channel, int, int, int, int, void *);
extern int channel_shell_request_cb(ssh_session, ssh_channel, void *);
extern int channel_exec_request_cb(ssh_session, ssh_channel, char const *, void *);
extern void bind_incoming_connection(ssh_bind, void *);
extern int session_service_request_cb(ssh_session, char const *, void *);
extern int session_auth_password_cb(ssh_session, char const *, char const *, void *);
extern int session_auth_pubkey_cb(ssh_session, char const *, struct ssh_key_struct *, char, void *);
extern ssh_channel session_channel_open_request_cb(ssh_session, void *);
Ssh::Server::Server(Genode::Env &env,
Genode::Xml_node const &config,
Ssh::Login_registry &logins)
:
_env(env), _heap(env.ram(), env.rm()), _logins(logins)
{
Libc::with_libc([&] {
_parse_config(config);
if (ssh_init() < 0) {
Genode::error("ssh_init failed.");
throw Init_failed();
}
_ssh_bind = ssh_bind_new();
if (!_ssh_bind) {
Genode::error("ssh_bind failed.");
throw Init_failed();
}
ssh_bind_options_set(_ssh_bind, SSH_BIND_OPTIONS_LOG_VERBOSITY, &_log_level);
ssh_bind_options_set(_ssh_bind, SSH_BIND_OPTIONS_BINDPORT, &_port);
_initialize_bind_callbacks();
_initialize_session_callbacks();
_initialize_channel_callbacks();
/*
* Always try to load all types of host key and error-out if
* the file is set but could not be loaded.
*/
try {
_load_hostkey(_rsa_key);
_load_hostkey(_ecdsa_key);
_load_hostkey(_ed25519_key);
} catch (...) {
Genode::error("loading keys failed.");
throw Init_failed();
}
_event_loop = ssh_event_new();
if (ssh_bind_listen(_ssh_bind) < 0) {
Genode::error("could not listen on port ", _port, ": ",
ssh_get_error(_ssh_bind));
throw Init_failed();
}
/* add AFTER(!) ssh_bind_listen call */
if (ssh_event_add_bind(_event_loop, _ssh_bind) < 0) {
Genode::error("unable to add server to event loop: ",
ssh_get_error(_ssh_bind));
throw Init_failed();
}
if (pthread_create(&_event_thread, nullptr, _server_loop, this)) {
Genode::error("could not create event thread");
throw Init_failed();
}
/* add pipe to wake up loop on late connecting terminal */
if (pipe(_server_fds) ||
ssh_event_add_fd(_event_loop,
_server_fds[0],
POLLIN,
write_avail_cb,
this) != SSH_OK ) {
Genode::error("Failed to create wakeup pipe");
throw -1;
}
Genode::log("Listen on port: ", _port);
}); /* Libc::with_libc */
}
Ssh::Server::~Server()
{
close(_server_fds[0]);
close(_server_fds[1]);
}
void Ssh::Server::_initialize_channel_callbacks()
{
Genode::memset(&_channel_cb, 0, sizeof(_channel_cb));
_channel_cb.userdata = this;
_channel_cb.channel_data_function = channel_data_cb;
_channel_cb.channel_env_request_function = channel_env_request_cb;
_channel_cb.channel_pty_request_function = channel_pty_request_cb;
_channel_cb.channel_pty_window_change_function = channel_pty_window_change_cb;
_channel_cb.channel_shell_request_function = channel_shell_request_cb;
_channel_cb.channel_exec_request_function = channel_exec_request_cb;
ssh_callbacks_init(&_channel_cb);
}
void Ssh::Server::_initialize_session_callbacks()
{
Genode::memset(&_session_cb, 0, sizeof(_session_cb));
_session_cb.userdata = this;
_session_cb.auth_password_function = session_auth_password_cb;
_session_cb.auth_pubkey_function = session_auth_pubkey_cb;
_session_cb.service_request_function = session_service_request_cb;
_session_cb.channel_open_request_session_function = session_channel_open_request_cb;
ssh_callbacks_init(&_session_cb);
}
void Ssh::Server::_initialize_bind_callbacks()
{
Genode::memset(&_bind_cb, 0, sizeof(_bind_cb));
_bind_cb.incoming_connection = bind_incoming_connection;
ssh_callbacks_init(&_bind_cb);
ssh_bind_set_callbacks(_ssh_bind, &_bind_cb, this);
}
void Ssh::Server::_cleanup_session(Session &s)
{
if (s.auth_sucessful) {
_log_logout(s);
}
ssh_channel_free(s.channel);
s.channel = nullptr;
ssh_event_remove_session(_event_loop, s.session);
ssh_disconnect(s.session);
ssh_free(s.session);
s.session = nullptr;
if (s.terminal) {
s.terminal->detach_channel();
}
try {
_request_terminal_reporter.generate([&] (Xml_generator& xml) {
xml.attribute("user", s.user());
xml.attribute("exit", "now");
});
} catch (...) {
Genode::warning("could not enable exit reporting");
}
Genode::destroy(&_heap, &s);
}
void Ssh::Server::_cleanup_sessions()
{
auto cleanup = [&] (Session &s) {
if (!ssh_is_connected(s.session)) {
_cleanup_session(s);
}
};
_sessions.for_each(cleanup);
}
void Ssh::Server::_parse_config(Genode::Xml_node const &config)
{
using Util::Filename;
_verbose = config.attribute_value("verbose", false);
_log_level = config.attribute_value("debug", 0u);
_log_logins = config.attribute_value("log_logins", true);
Genode::Lock::Guard g(_logins.lock());
auto print = [&] (Login const &login) {
Genode::log("Login configured: ", login);
};
_logins.for_each(print);
if (_config_once) { return; }
_config_once = true;
_port = config.attribute_value("port", 0u);
if (!_port) {
error("port invalid");
throw Invalid_config();
}
_allow_password = config.attribute_value("allow_password", false);
_allow_publickey = config.attribute_value("allow_publickey", false);
if (!_allow_password && !_allow_publickey) {
error("authentication methods missing");
throw Invalid_config();
}
_rsa_key = config.attribute_value("rsa_key", Filename());
_ecdsa_key = config.attribute_value("ecdsa_key", Filename());
_ed25519_key = config.attribute_value("ed25519_key", Filename());
Genode::log("Allowed auth methods: ",
_allow_password ? "password " : "",
_allow_publickey ? "public-key" : "");
}
void Ssh::Server::_load_hostkey(Util::Filename const &file)
{
if (file.valid() &&
ssh_bind_options_set(_ssh_bind, SSH_BIND_OPTIONS_HOSTKEY,
file.string()) < 0) {
Genode::error("could not load hostkey '", file, "'");
throw -1;
}
}
void *Ssh::Server::_server_loop(void *arg)
{
Ssh::Server *server = reinterpret_cast<Ssh::Server *>(arg);
server->loop();
return nullptr;
}
bool Ssh::Server::_allow_multi_login(ssh_session s, Login const &login)
{
if (login.multi_login) { return true; }
bool found = false;
auto lookup = [&] (Session const &s) {
if (s.user() == login.user) { found = true; }
};
_sessions.for_each(lookup);
return !found;
}
void Ssh::Server::_log_failed(char const *user, Session const &s, bool pubkey)
{
if (!_log_logins) { return; }
char const *date = Util::get_time();
Genode::log(date, " failed user ", user, " (", s.id(), ") ",
"with ", pubkey ? "public-key" : "password");
}
void Ssh::Server::_log_logout(Session const &s)
{
if (!_log_logins) { return; }
char const *date = Util::get_time();
Genode::log(date, " logout user ", s.user(), " (", s.id(), ")");
}
void Ssh::Server::_log_login(User const &user, Session const &s, bool pubkey)
{
if (!_log_logins) { return; }
char const *date = Util::get_time();
Genode::log(date, " login user ", user, " (", s.id(), ") ",
"with ", pubkey ? "public-key" : "password");
}
void Ssh::Server::attach_terminal(Ssh::Terminal &conn)
{
Genode::Lock::Guard g(_terminals.lock());
try {
new (&_heap) Terminal_session(_terminals,
conn, _event_loop);
} catch (...) {
Genode::error("could not attach Terminal for user ",
conn.user());
throw -1;
}
/* there might be sessions already waiting on the terminal */
bool attached = false;
auto lookup = [&] (Session &s) {
if (s.user() == conn.user() && !s.terminal) {
s.terminal = &conn;
s.terminal->attach_channel();
attached = true;
}
};
_sessions.for_each(lookup);
_wake_loop();
}
void Ssh::Server::detach_terminal(Ssh::Terminal &conn)
{
Genode::Lock::Guard g(_terminals.lock());
Terminal_session *p = nullptr;
auto lookup = [&] (Terminal_session &t) {
if (&t.conn == &conn) {
p = &t;
}
};
_terminals.for_each(lookup);
if (!p) {
Genode::error("could not detach Terminal for user ", conn.user());
return;
}
auto invalidate_terminal = [&] (Session &sess) {
Libc::with_libc([&] () {
ssh_blocking_flush(sess.session, 10000);
ssh_disconnect(sess.session);
if (sess.terminal != &conn) { return; }
sess.terminal = nullptr;
});
};
_sessions.for_each(invalidate_terminal);
_cleanup_sessions();
Genode::destroy(&_heap, p);
}
void Ssh::Server::update_config(Genode::Xml_node const &config)
{
Genode::Lock::Guard g(_terminals.lock());
_parse_config(config);
ssh_bind_options_set(_ssh_bind, SSH_BIND_OPTIONS_LOG_VERBOSITY, &_log_level);
}
Ssh::Terminal *Ssh::Server::lookup_terminal(Session &s)
{
Ssh::Terminal *p = nullptr;
auto lookup = [&] (Terminal_session &t) {
if (t.conn.user() == s.user()) { p = &t.conn; }
};
_terminals.for_each(lookup);
return p;
}
Ssh::Session *Ssh::Server::lookup_session(ssh_session s)
{
Session *p = nullptr;
auto lookup = [&] (Session &sess) {
if (sess.session == s) { p = &sess; }
};
_sessions.for_each(lookup);
return p;
}
bool Ssh::Server::request_terminal(Session &session,
const char* command)
{
Genode::Lock::Guard g(_logins.lock());
Login const *l = _logins.lookup(session.user().string());
if (!l || !l->request_terminal) {
return false;
}
try {
_request_terminal_reporter.generate([&] (Xml_generator& xml) {
xml.attribute("user", session.user());
if (command) {
xml.attribute("command", command);
}
});
} catch (...) {
Genode::warning("could not enable login reporting");
return false;
}
if (_log_logins) {
char const *date = Util::get_time();
Genode::log(date, " request Terminal for user ", session.user(),
" (", session.session, ")");
}
return true;
}
void Ssh::Server::incoming_connection(ssh_session s)
{
/*
* In case we get bombarded by incoming connections, deny
* all attempts when this arbritray level is reached.
*/
enum { MEM_RESERVE = 128u * 1024, };
if (_env.pd().avail_ram().value < (size_t)MEM_RESERVE) {
error("Too many connections");
throw -1;
}
new (&_heap) Session(_sessions, s, &_channel_cb, ++_session_id);
ssh_set_server_callbacks(s, &_session_cb);
int auth_methods = SSH_AUTH_METHOD_UNKNOWN;
auth_methods += _allow_password ? SSH_AUTH_METHOD_PASSWORD : 0;
auth_methods += _allow_publickey ? SSH_AUTH_METHOD_PUBLICKEY : 0;
ssh_set_auth_methods(s, auth_methods);
/*
* Normally we would check the result of the key exchange
* function but for better or worse using callbacks leads to
* a false negative. So ignore any result and move on in hope
* that the callsbacks will handle the situation.
*
* FIXME investigate why it somtimes fails in the first place.
*/
int key_exchange_result = ssh_handle_key_exchange(s);
if (SSH_OK != key_exchange_result) {
Genode::warning("key exchange returned ", key_exchange_result);
}
ssh_event_add_session(_event_loop, s);
}
bool Ssh::Server::auth_password(ssh_session s, char const *u, char const *pass)
{
Session *p = lookup_session(s);
if (!p || p->session != s) {
Genode::warning("session not found");
return false;
}
Session &session = *p;
/*
* Even if there is no valid login for the user, let
* the client try anyway and check multi login afterwards.
*/
Genode::Lock::Guard g(_logins.lock());
Login const *l = _logins.lookup(u);
if (l && l->user == u && l->password == pass) {
if (_allow_multi_login(s, *l)) {
session.bad_auth_attempts = 0;
session.auth_sucessful = true;
session.adopt(l->user);
_log_login(l->user, session, false);
return true;
} else {
ssh_disconnect(session.session);
_log_failed(u, session, false);
return false;
}
}
_log_failed(u, *p, false);
int &i = session.bad_auth_attempts;
if (++i >= _max_auth_attempts) {
if (_log_logins) {
char const *date = Util::get_time();
Genode::log(date, " disconnect user ", u, " (", session.id(),
") after ", i, " failed authentication attempts"
" with password");
}
ssh_disconnect(session.session);
}
return false;
}
bool Ssh::Server::auth_pubkey(ssh_session s, char const *u,
struct ssh_key_struct *pubkey,
char signature_state)
{
Session *p = lookup_session(s);
if (!p || p->session != s) {
Genode::warning("session not found");
return false;
}
Session &session = *p;
if (signature_state == SSH_PUBLICKEY_STATE_NONE) {
return SSH_AUTH_PARTIAL;
}
if (signature_state == SSH_PUBLICKEY_STATE_VALID) {
Genode::Lock::Guard g(_logins.lock());
Login const *l = _logins.lookup(u);
if (l && !ssh_key_cmp(pubkey, l->pub_key,
SSH_KEY_CMP_PUBLIC)) {
if (_allow_multi_login(s, *l)) {
session.auth_sucessful = true;
session.adopt(l->user);
_log_login(l->user, session, true);
return SSH_AUTH_SUCCESS;
}
}
}
_log_failed(u, session, true);
return SSH_AUTH_DENIED;
}
void Ssh::Server::loop()
{
while (true) {
int const events = ssh_event_dopoll(_event_loop, -1);
if (events == SSH_ERROR) {
_cleanup_sessions();
}
{
Genode::Lock::Guard g(_terminals.lock());
/* first remove all stale sessions */
auto cleanup = [&] (Session &s) {
if (ssh_is_connected(s.session)) { return ; }
_cleanup_session(s);
};
_sessions.for_each(cleanup);
/* second reset all active terminals */
auto reset_pending = [&] (Terminal_session &t) {
if (!t.conn.attached_channels()) { return; }
t.conn.reset_pending();
};
_terminals.for_each(reset_pending);
/*
* third send data on all sessions being attached
* to a terminal.
*/
auto send = [&] (Session &s) {
if (!s.terminal) { return; }
try { s.terminal->send(s.channel); }
catch (...) { _cleanup_session(s); }
};
_sessions.for_each(send);
}
}
}
void Ssh::Server::_wake_loop()
{
/* wake the event loop up */
Libc::with_libc([&] {
char c = 1;
::write(_server_fds[1], &c, sizeof(c));
});
}
static int write_avail_cb(socket_t fd, int revents, void *userdata)
{
int n = 0;
Libc::with_libc([&] {
char c;
n = ::read(fd, &c, sizeof(char));
});
return n;
}

View File

@ -0,0 +1,280 @@
/*
* \brief Component providing a Terminal session via SSH
* \author Josef Soentgen
* \author Pirmin Duss
* \date 2019-05-29
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
* Copyright (C) 2019 gapfruit AG
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
#ifndef _SSH_TERMINAL_SERVER_H_
#define _SSH_TERMINAL_SERVER_H_
/* Genode includes */
#include <base/log.h>
#include <base/registry.h>
#include <os/reporter.h>
/* libc includes */
#include <poll.h>
#include <pthread.h>
/* libssh includes */
#include <libssh/callbacks.h>
#include <libssh/libssh.h>
#include <libssh/server.h>
/* local includes */
#include "login.h"
#include "terminal.h"
namespace Ssh {
using namespace Genode;
struct Server;
struct Session;
struct Terminal_session;
struct Terminal_registry;
}
/**
* forward declaration of the write available callback.
*/
static int write_avail_cb(socket_t fd, int revents, void *userdata);
struct Ssh::Session : Genode::Registry<Session>::Element
{
User _user { };
uint32_t _id { 0 };
int bad_auth_attempts { 0 };
bool auth_sucessful { false };
ssh_session session { nullptr };
ssh_channel channel { nullptr };
ssh_channel_callbacks channel_cb { nullptr };
Ssh::Terminal *terminal { nullptr };
Genode::Lock _access_lock { };
Genode::Lock &lock_terminal()
{
return _access_lock;
}
bool spawn_terminal { false };
Session(Genode::Registry<Session> &reg,
ssh_session s,
ssh_channel_callbacks ccb,
uint32_t id)
: Element(reg, *this), _id(id), session(s), channel_cb(ccb) { }
void adopt(User const &user) { _user = user; }
User const &user() const { return _user; }
uint32_t id() const { return _id; }
void add_channel(ssh_channel c)
{
ssh_set_channel_callbacks(c, channel_cb);
channel = c;
}
};
struct Ssh::Terminal_session : Genode::Registry<Terminal_session>::Element
{
Ssh::Terminal &conn;
ssh_event _event_loop;
int _fds[2] { -1, -1 };
Terminal_session(Genode::Registry<Terminal_session> &reg,
Ssh::Terminal &conn,
ssh_event event_loop)
: Element(reg, *this), conn(conn), _event_loop(event_loop)
{
if (pipe(_fds) ||
ssh_event_add_fd(_event_loop,
_fds[0],
POLLIN,
write_avail_cb,
this) != SSH_OK ) {
Genode::error("Failed to create wakeup pipe");
throw -1;
}
conn.write_avail_fd = _fds[1];
}
~Terminal_session()
{
ssh_event_remove_fd(_event_loop, _fds[0]);
close(_fds[0]);
close(_fds[1]);
}
};
struct Ssh::Terminal_registry : Genode::Registry<Terminal_session>
{
Genode::Lock _lock { };
Genode::Lock &lock() { return _lock; }
};
class Ssh::Server
{
public:
struct Init_failed : Genode::Exception { };
struct Invalid_config : Genode::Exception { };
private:
using Session_registry = Genode::Registry<Session>;
Genode::Env &_env;
Genode::Heap _heap;
bool _verbose { false };
bool _allow_password { false };
bool _allow_publickey { false };
bool _log_logins { false };
int _max_auth_attempts { 3 };
unsigned _port { 0u };
unsigned _log_level { 0u };
int _server_fds[2] { -1, -1 };
bool _config_once { false };
ssh_bind _ssh_bind;
ssh_event _event_loop;
Util::Filename _rsa_key { };
Util::Filename _ecdsa_key { };
Util::Filename _ed25519_key { };
Expanding_reporter _request_terminal_reporter { _env,
"request_terminal",
"request_terminal" };
Terminal_registry _terminals { };
Login_registry &_logins;
pthread_t _event_thread;
/*
* Since we always pass ourself as userdata pointer, we may
* safely use the same callback for all sessions and channels.
*/
ssh_channel_callbacks_struct _channel_cb { };
ssh_server_callbacks_struct _session_cb { };
ssh_bind_callbacks_struct _bind_cb { };
Session_registry _sessions { };
uint32_t _session_id { 0 };
void _initialize_channel_callbacks();
void _initialize_session_callbacks();
void _initialize_bind_callbacks();
void _cleanup_session(Session &s);
void _cleanup_sessions();
void _parse_config(Genode::Xml_node const &config);
void _load_hostkey(Util::Filename const &file);
/*
* Event execution
*/
static void *_server_loop(void *arg);
bool _allow_multi_login(ssh_session s, Login const &login);
/********************
** Login messages **
********************/
void _log_failed(char const *user, Session const &s, bool pubkey);
void _log_logout(Session const &s);
void _log_login(User const &user, Session const &s, bool pubkey);
void _wake_loop();
public:
Server(Genode::Env &env,
Genode::Xml_node const &config,
Ssh::Login_registry &logins);
virtual ~Server();
void loop();
/***************************************************************
** Methods below are only used by Terminal session front end **
***************************************************************/
/**
* Attach Terminal session
*/
void attach_terminal(Ssh::Terminal &conn);
/**
* Detach Terminal session
*/
void detach_terminal(Ssh::Terminal &conn);
/**
* Update config
*/
void update_config(Genode::Xml_node const &config);
/*******************************************************
** Methods below are only used by callback back ends **
*******************************************************/
/**
* Look up Terminal for session
*/
Ssh::Terminal *lookup_terminal(Session &s);
/**
* Look up Session for SSH session
*/
Session *lookup_session(ssh_session s);
/**
* Request Terminal
*/
bool request_terminal(Session &session, const char* command = nullptr);
/**
* Handle new incoming connections
*/
void incoming_connection(ssh_session s);
/**
* Handle password authentication
*/
bool auth_password(ssh_session s, char const *u, char const *pass);
/**
* Handle public-key authentication
*/
bool auth_pubkey(ssh_session s, char const *u,
struct ssh_key_struct *pubkey,
char signature_state);
};
#endif /* _SSH_TERMINAL_SERVER_H_ */

View File

@ -0,0 +1,103 @@
/*
* \brief Component providing a Terminal session via SSH
* \author Josef Soentgen
* \author Pirmin Duss
* \date 2019-05-29
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
* Copyright (C) 2019 gapfruit AG
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
#ifndef _SSH_TERMINAL_SESSION_COMPONENT_H_
#define _SSH_TERMINAL_SESSION_COMPONENT_H_
/* Genode includes */
#include <base/capability.h>
#include <base/signal.h>
/* local includes */
#include "util.h"
#include "terminal.h"
namespace Terminal {
class Session_component;
};
class Terminal::Session_component : public Genode::Rpc_object<Session, Session_component>,
public Ssh::Terminal
{
private:
Genode::Attached_ram_dataspace _io_buffer;
public:
Session_component(Genode::Env &env,
Genode::size_t io_buffer_size,
Ssh::User const &user)
:
Ssh::Terminal(user),
_io_buffer(env.ram(), env.rm(), io_buffer_size)
{ }
virtual ~Session_component() = default;
/********************************
** Terminal session interface **
********************************/
Genode::size_t read(void *buf, Genode::size_t) override { return 0; }
Genode::size_t write(void const *buf, Genode::size_t) override { return 0; }
Size size() override { return Ssh::Terminal::size(); }
bool avail() override { return !Ssh::Terminal::read_buffer_empty(); }
void read_avail_sigh(Genode::Signal_context_capability sigh) override {
Ssh::Terminal::read_avail_sigh(sigh);
}
void connected_sigh(Genode::Signal_context_capability sigh) override {
Ssh::Terminal::connected_sigh(sigh);
}
void size_changed_sigh(Genode::Signal_context_capability sigh) override {
Ssh::Terminal::size_changed_sigh(sigh);
}
Genode::Dataspace_capability _dataspace() { return _io_buffer.cap(); }
Genode::size_t _read(Genode::size_t num)
{
Genode::size_t num_bytes = 0;
Libc::with_libc([&] () {
char *buf = _io_buffer.local_addr<char>();
num = Genode::min(_io_buffer.size(), num);
num_bytes = Ssh::Terminal::read(buf, num);
});
return num_bytes;
}
Genode::size_t _write(Genode::size_t num)
{
ssize_t written = 0;
Libc::with_libc([&] () {
char *buf = _io_buffer.local_addr<char>();
num = Genode::min(num, _io_buffer.size());
written = Ssh::Terminal::write(buf, num);
if (written < 0) {
Genode::error("write error, dropping data");
written = 0;
}
});
return written;
}
};
#endif /* _SSH_TERMINAL_SESSION_COMPONENT_H_ */

View File

@ -0,0 +1,305 @@
/*
* \brief Component providing a Terminal session via SSH
* \author Josef Soentgen
* \author Pirmin Duss
* \author Sid Hussmann
* \date 2019-05-29
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
* Copyright (C) 2019 gapfruit AG
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
/* libssh includes */
#include <libssh/callbacks.h>
#include <libssh/libssh.h>
#include <libssh/server.h>
/* local includes */
#include "server.h"
/***********************
** Channel callbacks **
***********************/
/**
* Handle SSH channel data request
*/
int channel_data_cb(ssh_session session, ssh_channel channel,
void *data, uint32_t len, int is_stderr,
void *userdata)
{
using Genode::error;
using Genode::Lock;
if (len == 0) {
return 0;
}
Ssh::Server &server = *reinterpret_cast<Ssh::Server*>(userdata);
Ssh::Session *p = server.lookup_session(session);
if (!p) {
error("session not found");
return SSH_ERROR;
}
if (p->channel != channel) {
error("wrong channel");
return SSH_ERROR;
}
if (!p->terminal) {
error("no terminal");
return SSH_ERROR;
}
Ssh::Terminal &conn { *p->terminal };
Lock::Guard guard { conn.read_buf.lock() };
char const *src { reinterpret_cast<char const*>(data) };
size_t num_bytes { 0 };
while ((conn.read_buf.write_avail() > 0) && (num_bytes < len)) {
char c = src[num_bytes];
/* replace ^? with ^H and let's hope we do not break anything */
enum { DEL = 0x7f, BS = 0x08, };
if (c == DEL) {
conn.read_buf.append(BS);
} else {
conn.read_buf.append(c);
}
num_bytes++;
}
conn.notify_read_avail();
return num_bytes;
}
/**
* Handle SSH channel shell request
*
* For now we ignore this request because there is no way to change the
* $ENV of the Terminal::Session client currently.
*/
int channel_env_request_cb(ssh_session session, ssh_channel channel,
char const *env_name, char const *env_value,
void *userdata)
{
return SSH_OK;
}
/**
* Handle SSH channel PTY request
*/
int channel_pty_request_cb(ssh_session session, ssh_channel channel,
char const *term,
int cols, int rows, int py, int px,
void *userdata)
{
using namespace Genode;
Ssh::Server &server = *reinterpret_cast<Ssh::Server*>(userdata);
Ssh::Session *p = server.lookup_session(session);
if (!p || p->channel != channel) { return SSH_ERROR; }
/*
* Look up terminal and in case there is none, check
* if we have to wait for another subsystem to come up.
* In this case we return successfully to the client
* and wait for a Terminal session to be established.
*/
if (!p->terminal) {
p->terminal = server.lookup_terminal(*p);
if (!p->terminal) {
return server.request_terminal(*p) ? SSH_OK
: SSH_ERROR;
}
}
p->terminal->attach_channel();
Ssh::Terminal &conn = *p->terminal;
conn.size(Terminal::Session::Size(cols, rows));
conn.notify_size_changed();
/* session handling already takes care of having a terminal attached */
conn.notify_connected();
return SSH_OK;
}
/**
* Handle SSH channel PTY resize request
*/
int channel_pty_window_change_cb(ssh_session session, ssh_channel channel,
int width, int height, int pxwidth, int pwheight,
void *userdata)
{
(void)pxwidth;
(void)pwheight;
using namespace Genode;
Ssh::Server &server = *reinterpret_cast<Ssh::Server*>(userdata);
Ssh::Session *p = server.lookup_session(session);
if (!p || p->channel != channel || !p->terminal) { return SSH_ERROR; }
Ssh::Terminal &conn = *p->terminal;
conn.size(Terminal::Session::Size(width, height));
conn.notify_size_changed();
return SSH_OK;
}
/**
* Handle SSH channel shell request
*
* For now we ignore this request as the shell is implicitly provided when
* the PTY request is handled.
*/
int channel_shell_request_cb(ssh_session session, ssh_channel channel,
void *userdata)
{
return SSH_OK;
}
/**
* Handle SSH channel exec request
*
* Exec requests provide a command that needs to be executed.
* The command is provided while starting a new terminal using
* request_terminal().
*/
int channel_exec_request_cb(ssh_session session, ssh_channel channel,
const char *command,
void *userdata)
{
using namespace Genode;
Ssh::Server &server = *reinterpret_cast<Ssh::Server*>(userdata);
Ssh::Session *p = server.lookup_session(session);
if (!p || p->channel != channel) { return SSH_ERROR; }
/*
* Look up terminal and in case there is none, check
* if we have to wait for another subsystem to come up.
* In this case we return successfully to the client
* and wait for a Terminal session to be established.
*/
if (!p->terminal) {
p->terminal = server.lookup_terminal(*p);
if (!p->terminal) {
return server.request_terminal(*p, command) ? SSH_OK
: SSH_ERROR;
}
}
/* exec commands can only be done with newly started terminals */
return SSH_ERROR;
}
/***********************
** Session callbacks **
***********************/
/**
* Handle SSH session service requests
*/
int session_service_request_cb(ssh_session,
char const *service, void*)
{
return Genode::strcmp(service, "ssh-userauth") == 0 ? 0 : -1;
}
/**
* Handle SSH session password authentication requests
*/
int session_auth_password_cb(ssh_session session,
char const *user, char const *password,
void *userdata)
{
Ssh::Server &server = *reinterpret_cast<Ssh::Server*>(userdata);
return server.auth_password(session, user, password) ? SSH_AUTH_SUCCESS
: SSH_AUTH_DENIED;
}
/**
* Handle SSH session public-key authentication requests
*/
int session_auth_pubkey_cb(ssh_session session, char const *user,
struct ssh_key_struct *pubkey,
char state, void *userdata)
{
Ssh::Server &server = *reinterpret_cast<Ssh::Server*>(userdata);
return server.auth_pubkey(session, user, pubkey, state) ? SSH_AUTH_SUCCESS
: SSH_AUTH_DENIED;
}
/**
* Handle SSH session open channel requests
*/
ssh_channel session_channel_open_request_cb(ssh_session session,
void *userdata)
{
using namespace Genode;
Ssh::Server &server = *reinterpret_cast<Ssh::Server*>(userdata);
Ssh::Session *p = server.lookup_session(session);
if (!p) {
error("could not look up session");
return nullptr;
}
/* for now only one channel */
if (p->channel) {
log("Only one channel per session supported");
return nullptr;
}
ssh_channel channel = ssh_channel_new(p->session);
if (!channel) {
error("could not create new channel: '", ssh_get_error(p->session));
return nullptr;
}
p->add_channel(channel);
return channel;
}
/**
* Handle new incoming SSH session requests
*/
void bind_incoming_connection(ssh_bind sshbind, void *userdata)
{
using namespace Genode;
ssh_session session = ssh_new();
if (!session || ssh_bind_accept(sshbind, session)) {
error("could not accept session: '", ssh_get_error(session), "'");
ssh_free(session);
return;
}
Ssh::Server &server = *reinterpret_cast<Ssh::Server*>(userdata);
try {
server.incoming_connection(session);
}
catch (...) {
ssh_disconnect(session);
ssh_free(session);
}
}

View File

@ -1,5 +1,8 @@
TARGET = ssh_terminal
SRC_CC = main.cc
LIBS = base libc libssh libc_pipe
TARGET = ssh_terminal
SRC_CC = main.cc
SRC_CC += server.cc
SRC_CC += ssh_callbacks.cc
SRC_CC += util.cc
LIBS = base libc libssh libc_pipe
CC_CXX_WARN_STRICT =

View File

@ -0,0 +1,255 @@
/*
* \brief Component providing a Terminal session via SSH
* \author Josef Soentgen
* \author Pirmin Duss
* \date 2019-05-29
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
* Copyright (C) 2019 gapfruit AG
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
#ifndef _SSH_TERMINAL_TERMINAL_H_
#define _SSH_TERMINAL_TERMINAL_H_
/* Genode includes */
#include <base/capability.h>
#include <base/signal.h>
#include <session/session.h>
#include <base/log.h>
#include <terminal_session/terminal_session.h>
/* local includes */
#include "login.h"
namespace Ssh
{
using namespace Genode;
class Terminal;
}
class Ssh::Terminal
{
public:
Util::Buffer<4096u> read_buf { };
int write_avail_fd { -1 };
private:
Util::Buffer<4096u> _write_buf { };
::Terminal::Session::Size _size { 0, 0 };
Signal_context_capability _size_changed_sigh;
Signal_context_capability _connected_sigh;
Signal_context_capability _read_avail_sigh;
Ssh::User const _user { };
unsigned _attached_channels { 0u };
unsigned _pending_channels { 0u };
public:
/**
* Constructor
*/
Terminal(Ssh::User const &user) : _user(user) { }
virtual ~Terminal() = default;
Ssh::User const &user() const { return _user; }
unsigned attached_channels() const { return _attached_channels; }
void attach_channel() { ++_attached_channels; }
void detach_channel() { --_attached_channels; }
void reset_pending() { _pending_channels = 0; }
/*********************************
** Terminal::Session interface **
*********************************/
/**
* Register signal handler to be notified once the size was changed
*/
void size_changed_sigh(Signal_context_capability sigh) {
_size_changed_sigh = sigh; }
/**
* Register signal handler to be notified once we accepted the TCP
* connection
*/
void connected_sigh(Signal_context_capability sigh)
{
_connected_sigh = sigh;
if (_attached_channels > 0) {
notify_connected();
}
}
/**
* Register signal handler to be notified when data is available for
* reading
*/
void read_avail_sigh(Signal_context_capability sigh)
{
_read_avail_sigh = sigh;
/* if read data is available right now, deliver signal immediately */
if (read_buffer_empty() && _read_avail_sigh.valid()) {
Signal_transmitter(_read_avail_sigh).submit();
}
}
/**
* Inform client about the finished initialization of the SSH
* session
*/
void notify_connected()
{
if (!_connected_sigh.valid()) { return; }
Signal_transmitter(_connected_sigh).submit();
}
/**
* Inform client about avail data
*/
void notify_read_avail()
{
if (!_read_avail_sigh.valid()) { return; }
Signal_transmitter(_read_avail_sigh).submit();
}
/**
* Inform client about the changed size of the remote terminal
*/
void notify_size_changed()
{
if (!_size_changed_sigh.valid()) { return; }
Signal_transmitter(_size_changed_sigh).submit();
}
/**
* Set size of the Terminal session to match remote terminal
*/
void size(::Terminal::Session::Size size) { _size = size; }
/**
* Return size of the Terminal session
*/
::Terminal::Session::Size size() const { return _size; }
/*****************
** I/O methods **
*****************/
/**
* Send internal write buffer content to SSH channel
*/
void send(ssh_channel channel)
{
Lock::Guard g(_write_buf.lock());
if (!_write_buf.read_avail()) { return; }
/* ignore send request */
if (!channel || !ssh_channel_is_open(channel)) { return; }
char const *src = _write_buf.content();
size_t const len = _write_buf.read_avail();
/* XXX we do not handle partial writes */
int const num_bytes = ssh_channel_write(channel, src, len);
if (num_bytes && (size_t)num_bytes < len) {
warning("send on channel was truncated");
}
if (++_pending_channels >= _attached_channels) {
_write_buf.reset();
}
/* at this point the client might have disconnected */
if (num_bytes < 0) { throw -1; }
}
/******************************************
** Methods called by Terminal front end **
******************************************/
/**
* Read out internal read buffer and copy into destination buffer.
*/
size_t read(char *dst, size_t dst_len)
{
Genode::Lock::Guard g(read_buf.lock());
size_t const num_bytes = min(dst_len, read_buf.read_avail());
Genode::memcpy(dst, read_buf.content(), num_bytes);
read_buf.consume(num_bytes);
/* notify client if there are still bytes available for reading */
if (!read_buf.read_avail()) { read_buf.reset(); }
else {
if (_read_avail_sigh.valid()) {
Signal_transmitter(_read_avail_sigh).submit();
}
}
return num_bytes;
}
/**
* Write into internal buffer and copy to underlying socket
*/
size_t write(char const *src, Genode::size_t src_len)
{
Lock::Guard g(_write_buf.lock());
size_t num_bytes = 0;
size_t i = 0;
Libc::with_libc([&] {
while (_write_buf.write_avail() > 0 && i < src_len) {
char c = src[i];
if (c == '\n') {
_write_buf.append('\r');
}
_write_buf.append(c);
num_bytes++;
i++;
}
});
/* wake the event loop up */
Libc::with_libc([&] {
char c = 1;
::write(write_avail_fd, &c, sizeof(c));
});
return num_bytes;
}
/**
* Return true if the internal read buffer is ready to receive data
*/
bool read_buffer_empty()
{
Genode::Lock::Guard g(read_buf.lock());
return !read_buf.read_avail();
}
};
#endif /* _SSH_TERMINAL_TERMINAL_H_ */

View File

@ -0,0 +1,36 @@
/*
* \brief Component providing a Terminal session via SSH
* \author Josef Soentgen
* \author Pirmin Duss
* \date 2019-05-29
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
* Copyright (C) 2019 gapfruit AG
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
/* local includes */
#include "util.h"
char const *Util::get_time()
{
static char buffer[32];
char const *p = "<invalid date>";
Libc::with_libc([&] {
struct timespec ts;
if (clock_gettime(0, &ts)) { return; }
struct tm *tm = localtime((time_t*)&ts.tv_sec);
if (!tm) { return; }
size_t const n = strftime(buffer, sizeof(buffer), "%F %H:%M:%S", tm);
if (n > 0 && n < sizeof(buffer)) { p = buffer; }
}); /* Libc::with_libc */
return p;
}

View File

@ -0,0 +1,61 @@
/*
* \brief Component providing a Terminal session via SSH
* \author Josef Soentgen
* \author Pirmin Duss
* \date 2019-05-29
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
* Copyright (C) 2019 gapfruit AG
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
#ifndef _SSH_TERMINAL_UTIL_H_
#define _SSH_TERMINAL_UTIL_H_
/* Genode includes */
#include <util/string.h>
#include <libc/component.h>
/* libc includes */
#include <unistd.h>
#include <time.h>
namespace Util
{
using Filename = Genode::String<256>;
template <size_t C>
struct Buffer;
/*
* get the current time from the libc backend.
*/
char const *get_time();
}
template <size_t C>
struct Util::Buffer
{
Genode::Lock _lock { };
char _data[C] { };
size_t _head { 0 };
size_t _tail { 0 };
size_t read_avail() const { return _head > _tail ? _head - _tail : 0; }
size_t write_avail() const { return _head <= C ? C - _head : 0; }
char const *content() const { return &_data[_tail]; }
void append(char c) { _data[_head++] = c; }
void consume(size_t n) { _tail += n; }
void reset() { _head = _tail = 0; }
Genode::Lock &lock() { return _lock; }
};
#endif /* _SSH_TERMINAL_UTIL_H_ */

View File

@ -0,0 +1,161 @@
/*
* \brief Component starting noux in a sub-init to execute a specific command
* \author Sid Hussmann
* \date 2019-05-11
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
* Copyright (C) 2019 gapfruit AG
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
/*
* Copyright (C) 2019 gapfruit AG
*/
/* Genode includes */
#include <base/log.h>
#include <base/component.h>
#include <base/attached_rom_dataspace.h>
#include <os/reporter.h>
/* local includes */
namespace Exec_terminal {
class Main;
using namespace Genode;
}
class Exec_terminal::Main
{
private:
Env& _env;
Attached_rom_dataspace _exec_terminal_config { _env, "exec_terminal.config" };
Signal_handler<Main> _exec_terminal_config_handler { _env.ep(), *this, &Main::_handle_exec_terminal_config };
Expanding_reporter _init_config_reporter { _env, "config", "config" };
unsigned int _version { 0 };
void _handle_exec_terminal_config();
public:
Main(Env& env) :
_env{env}
{
_exec_terminal_config.sigh(_exec_terminal_config_handler);
_handle_exec_terminal_config();
}
virtual ~Main() = default;
};
void Exec_terminal::Main::_handle_exec_terminal_config()
{
_exec_terminal_config.update();
const Xml_node cfg = _exec_terminal_config.xml();
Genode::log(cfg);
if (!cfg.has_type("empty")) {
if (cfg.has_attribute("exit")) {
_init_config_reporter.generate([&] (Xml_generator& xml) {
xml.node("empty");
});
} else {
_init_config_reporter.generate([&] (Xml_generator& xml) {
xml.node("parent-provides",[&] () {
xml.node("service",[&] () { xml.attribute("name", "CPU"); });
xml.node("service",[&] () { xml.attribute("name", "File_system"); });
xml.node("service",[&] () { xml.attribute("name", "LOG"); });
xml.node("service",[&] () { xml.attribute("name", "PD"); });
xml.node("service",[&] () { xml.attribute("name", "RM"); });
xml.node("service",[&] () { xml.attribute("name", "ROM"); });
xml.node("service",[&] () { xml.attribute("name", "Report"); });
xml.node("service",[&] () { xml.attribute("name", "Terminal"); });
xml.node("service",[&] () { xml.attribute("name", "Timer"); });
});
xml.node("start",[&] () {
xml.attribute("name", "noux");
xml.attribute("caps", "500");
xml.attribute("version", ++_version);
xml.node("resource",[&] () { xml.attribute("name", "RAM"); xml.attribute("quantum", "64M"); });
xml.node("config",[&] () {
xml.node("fstab",[&] () {
xml.node("tar",[&] () { xml.attribute("name", "bash.tar"); });
xml.node("tar",[&] () { xml.attribute("name", "coreutils-minimal.tar"); });
xml.node("tar",[&] () { xml.attribute("name", "vim-minimal.tar"); });
xml.node("dir",[&] () {
xml.attribute("name", "rw");
xml.node("fs",[&] () { xml.attribute("label", "rw"); });
});
xml.node("dir",[&] () {
xml.attribute("name", "tmp");
xml.node("ram",[&] () { });
});
});
xml.node("start",[&] () {
xml.attribute("name", "/bin/bash");
xml.node("env",[&] () {
xml.attribute("name", "TERM");
xml.attribute("value", "screen");
});
xml.node("env",[&] () {
xml.attribute("name", "HOME");
xml.attribute("value", "/");
});
xml.node("env",[&] () {
xml.attribute("name", "IGNOREOF");
xml.attribute("value", "3");
});
if (cfg.has_attribute("command")) {
Genode::String<128> cmd;
cfg.attribute_value("command", &cmd);
if (cmd.valid()) {
xml.node("arg",[&] () {
xml.attribute("value", "-c");
});
xml.node("arg",[&] () {
// FIXME appending " ; true" is done to force bash to fork.
// noux will fail otherwise. This invalidates any exit codes.
xml.attribute("value", Genode::String<136>(cmd, " ; true"));
});
}
} else {
xml.node("env",[&] () {
xml.attribute("name", "PS1");
xml.attribute("value", "noux@$PWD> ");
});
}
});
});
xml.node("route",[&] () {
xml.node("service",[&] () { xml.attribute("name", "CPU"); xml.node("parent",[&] () {}); });
xml.node("service",[&] () { xml.attribute("name", "File_system"); xml.node("parent",[&] () {}); });
xml.node("service",[&] () { xml.attribute("name", "LOG"); xml.node("parent",[&] () {}); });
xml.node("service",[&] () { xml.attribute("name", "PD"); xml.node("parent",[&] () {}); });
xml.node("service",[&] () { xml.attribute("name", "RM"); xml.node("parent",[&] () {}); });
xml.node("service",[&] () { xml.attribute("name", "ROM"); xml.node("parent",[&] () {}); });
xml.node("service",[&] () { xml.attribute("name", "Terminal"); xml.node("parent",[&] () {}); });
xml.node("service",[&] () { xml.attribute("name", "Timer"); xml.node("parent",[&] () {}); });
});
});
});
}
}
}
void Component::construct(Genode::Env &env) {
static Exec_terminal::Main main(env);
}

View File

@ -0,0 +1,3 @@
TARGET = exec_terminal
SRC_CC = main.cc
LIBS += base