/*
 * \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;
}
