diff --git a/include/multipass/ssh/libssh_wrapper.h b/include/multipass/ssh/libssh_wrapper.h index 9ac17edb4b..369800361c 100644 --- a/include/multipass/ssh/libssh_wrapper.h +++ b/include/multipass/ssh/libssh_wrapper.h @@ -83,6 +83,7 @@ class Libssh : public Singleton int* core_dumped) const; // --- channel callbacks --------------------------------------------------- + virtual void ssh_callbacks_initialize(ssh_channel_callbacks callbacks) const; virtual int ssh_add_channel_callbacks(ssh_channel channel, ssh_channel_callbacks cb) const; virtual int ssh_remove_channel_callbacks(ssh_channel channel, ssh_channel_callbacks cb) const; diff --git a/include/multipass/ssh/plain_sftp_session.h b/include/multipass/ssh/plain_sftp_session.h new file mode 100644 index 0000000000..e6b616157f --- /dev/null +++ b/include/multipass/ssh/plain_sftp_session.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include +#include +#include + +#include // TODO@sftp avoid this include (need to go through MP_LIBSSH) + +namespace multipass +{ + +/** + * A concrete SftpSession backed by an SSHFS mount: it serves the SFTP protocol over an SSH + * session to a remote sshfs client, which mounts it on the guest. + */ +class PlainSftpSession : public SftpSession, public PrivatePassProvider +{ +public: + PlainSftpSession(PlainSSHSession&& ssh_session_obj, const std::string& sshfs_cmd); + PlainSftpSession(const PlainSftpSession&) = delete; + PlainSftpSession& operator=(const PlainSftpSession&) = delete; + + // TODO@sftp Make class final before enabling these + PlainSftpSession(PlainSftpSession&&) = delete; + PlainSftpSession& operator=(PlainSftpSession&&) = delete; + +private: + // TODO@sftp avoid mentioning sftp_server_free here (need to go through MP_LIBSSH) + using RawSftpSessionUptr = std::unique_ptr; + + static RawSftpSessionUptr make_raw_sftp_session(ssh_session raw_session, ssh_channel channel); + + PlainSSHSession plain_ssh_session; + std::unique_ptr sshfs_process; + RawSftpSessionUptr raw_sftp_session; +}; +} // namespace multipass diff --git a/include/multipass/ssh/plain_ssh_process.h b/include/multipass/ssh/plain_ssh_process.h index fe629ed661..1b97af3b95 100644 --- a/include/multipass/ssh/plain_ssh_process.h +++ b/include/multipass/ssh/plain_ssh_process.h @@ -19,6 +19,8 @@ #include +#include + #include #include @@ -28,12 +30,14 @@ namespace multipass { +class PlainSftpSession; + class PlainSSHProcess : public SSHProcess { public: using ChannelUPtr = std::unique_ptr; - PlainSSHProcess(ssh_session_struct& ssh_session, + PlainSSHProcess(ssh_session_struct& raw_session, const std::string& cmd, std::unique_lock session_lock); @@ -56,6 +60,12 @@ class PlainSSHProcess : public SSHProcess std::string read_std_error() override; const std::string& get_cmd() const override; +public: // but restricted + // Obtain a non-owning libssh channel handle. + // The caller adopts thread-safety responsibility for the channel with respect to this + // SSHProcess and the SSHSession it belongs to. + ssh_channel borrow_channel(const PrivatePassProvider::PrivatePass&); + private: enum class StreamType { @@ -70,7 +80,7 @@ class PlainSSHProcess : public SSHProcess // ensure thread safety std::unique_lock session_lock; // do not attempt to re-lock, as this is moved from - ssh_session session; + ssh_session raw_session; std::string cmd; ChannelUPtr channel; std::variant exit_result; diff --git a/include/multipass/ssh/plain_ssh_session.h b/include/multipass/ssh/plain_ssh_session.h index 77423573f5..86ee1f674f 100644 --- a/include/multipass/ssh/plain_ssh_session.h +++ b/include/multipass/ssh/plain_ssh_session.h @@ -17,9 +17,10 @@ #pragma once -#include #include +#include + #include #include @@ -29,7 +30,10 @@ namespace multipass { class SSHKeyProvider; -class PlainSSHSession : public SSHSession +class PlainSftpSession; +class PlainSSHProcess; + +class PlainSSHSession final : public SSHSession // final to prevent chopping on move { public: PlainSSHSession(const std::string& host, @@ -49,10 +53,21 @@ class PlainSSHSession : public SSHSession /** * @copydoc SSHSession::exec + * + * The dynamic type is always a PlainSSHProcess; see exec_plain to obtain it statically. */ [[nodiscard]] std::unique_ptr exec(const std::string& cmd, bool whisper = false) override; + /** + * TODO@sftp can we copydoc? partially + * Like exec, but statically typed to the concrete PlainSSHProcess this session produces. + */ + [[nodiscard]] std::unique_ptr exec_plain(const std::string& cmd, + bool whisper = false); + + std::unique_ptr make_sftp_session(const std::string& sshfs_cmd) && override; + /** * @copydoc SSHSession::is_connected */ @@ -63,15 +78,25 @@ class PlainSSHSession : public SSHSession */ [[nodiscard]] bool is_moved() const override; - operator ssh_session() override; + operator ssh_session() override; // TODO@sftp remove void force_shutdown() override; // TODO@sftp this should not be public +public: // but restricted + /** + * Obtain a non-owning libssh session handle. + * The caller adopts thread-safety responsibility for the underlying session with respect to + * this SSHSession + * + * @pre !this->is_moved() + */ + ssh_session borrow_session(const PrivatePassProvider::PrivatePass&) const; + private: PlainSSHSession(PlainSSHSession&&, std::unique_lock lock); void set_option(ssh_options_e type, const void* value); - std::unique_ptr session; + std::unique_ptr raw_session; mutable std::mutex mut; }; } // namespace multipass diff --git a/include/multipass/ssh/sftp_client.h b/include/multipass/ssh/sftp_client.h index dfc91964cc..a432861dcd 100644 --- a/include/multipass/ssh/sftp_client.h +++ b/include/multipass/ssh/sftp_client.h @@ -51,7 +51,7 @@ class SFTPClient int port, const std::string& username, const std::string& priv_key_blob); - SFTPClient(SSHSessionUPtr ssh_session); + SFTPClient(SSHSessionUPtr ssh_session_obj); virtual bool is_remote_dir(const fs::path& path); virtual bool push(const fs::path& source_path, const fs::path& target_path, Flags flags = {}); @@ -69,7 +69,7 @@ class SFTPClient void do_push_file(std::istream& source, const fs::path& target_path); void do_pull_file(const fs::path& source_path, std::ostream& target); - SSHSessionUPtr ssh_session; + SSHSessionUPtr ssh_session_obj; SFTPSessionUPtr sftp; }; diff --git a/include/multipass/ssh/ssh_client.h b/include/multipass/ssh/ssh_client.h index 03cb231657..05637ae928 100644 --- a/include/multipass/ssh/ssh_client.h +++ b/include/multipass/ssh/ssh_client.h @@ -42,7 +42,7 @@ class SSHClient const std::string& username, const std::string& priv_key_blob, ConsoleCreator console_creator); - SSHClient(SSHSessionUPtr ssh_session, ConsoleCreator console_creator); + SSHClient(SSHSessionUPtr ssh_session_obj, ConsoleCreator console_creator); int exec(const std::vector>& args_list); int connect(); @@ -52,7 +52,7 @@ class SSHClient int exec_string(const std::string& cmd_line); int get_ssh_exit_code(); - SSHSessionUPtr ssh_session; + SSHSessionUPtr ssh_session_obj; ChannelUPtr channel; Console::UPtr console; }; diff --git a/include/multipass/ssh/ssh_session.h b/include/multipass/ssh/ssh_session.h index ab13673b5d..14a6d9c681 100644 --- a/include/multipass/ssh/ssh_session.h +++ b/include/multipass/ssh/ssh_session.h @@ -26,11 +26,17 @@ namespace multipass { +class SftpSession; + class SSHSession { public: virtual ~SSHSession() = default; + // Non-copyable (but movable by descendants, see below) + SSHSession(const SSHSession&) = delete; + SSHSession& operator=(const SSHSession&) = delete; + /** * Execute a command in this SSH session. * @@ -49,6 +55,8 @@ class SSHSession [[nodiscard]] virtual std::unique_ptr exec(const std::string& cmd, bool whisper = false) = 0; + virtual std::unique_ptr make_sftp_session(const std::string& sshfs_cmd) && = 0; + /** * @return Whether this object represents a session that is currently connected */ @@ -65,9 +73,6 @@ class SSHSession protected: SSHSession() = default; - - SSHSession(const SSHSession&) = delete; - SSHSession& operator=(const SSHSession&) = delete; SSHSession(SSHSession&&) = default; SSHSession& operator=(SSHSession&&) = default; }; diff --git a/include/multipass/sshfs_mount/sftp_session.h b/include/multipass/sshfs_mount/sftp_session.h new file mode 100644 index 0000000000..85a8c898bb --- /dev/null +++ b/include/multipass/sshfs_mount/sftp_session.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +namespace multipass +{ +/** + * A server-side SFTP session. + */ +class SftpSession +{ +public: + virtual ~SftpSession() = default; + + // No copies + SftpSession(const SftpSession&) = delete; + SftpSession& operator=(const SftpSession&) = delete; + +protected: + SftpSession() = default; +}; +} // namespace multipass diff --git a/src/ssh/CMakeLists.txt b/src/ssh/CMakeLists.txt index 65b3b7cc1d..f44dd0c85d 100644 --- a/src/ssh/CMakeLists.txt +++ b/src/ssh/CMakeLists.txt @@ -36,10 +36,14 @@ function(add_ssh_target TARGET_NAME LIBSSH_TARGET) add_library(${TARGET_NAME} STATIC libssh_scope_guard.cpp openssh_key_provider.cpp + plain_sftp_session.cpp plain_ssh_process.cpp plain_ssh_session.cpp ssh_client_key_provider.cpp) + # TODO@sftp move remaining bits to MP_LIBSSH, then this should not be necessary + target_compile_definitions(${TARGET_NAME} PRIVATE WITH_SERVER) + target_link_libraries(${TARGET_NAME} ${LIBSSH_TARGET} fmt::fmt-header-only @@ -60,6 +64,8 @@ function(add_sftp_client_target TARGET_NAME LIBSSH_TARGET) sftp_utils.cpp plain_ssh_session.cpp) + target_compile_definitions(${TARGET_NAME} PRIVATE WITH_SERVER) + target_link_libraries(${TARGET_NAME} ${LIBSSH_TARGET} fmt::fmt-header-only @@ -78,6 +84,8 @@ function(add_ssh_client_target TARGET_NAME LIBSSH_TARGET) ssh_client.cpp plain_ssh_session.cpp) + target_compile_definitions(${TARGET_NAME} PRIVATE WITH_SERVER) + target_link_libraries(${TARGET_NAME} ${LIBSSH_TARGET} console diff --git a/src/ssh/libssh_wrapper.cpp b/src/ssh/libssh_wrapper.cpp index fa457669f4..d95f258fd2 100644 --- a/src/ssh/libssh_wrapper.cpp +++ b/src/ssh/libssh_wrapper.cpp @@ -185,6 +185,11 @@ int mp::Libssh::ssh_channel_get_exit_state(ssh_channel channel, } // --- channel callbacks ------------------------------------------------------ +void mp::Libssh::ssh_callbacks_initialize(ssh_channel_callbacks callbacks) const +{ + ssh_callbacks_init(callbacks); +} + int mp::Libssh::ssh_add_channel_callbacks(ssh_channel channel, ssh_channel_callbacks cb) const { return ::ssh_add_channel_callbacks(channel, cb); diff --git a/src/ssh/plain_sftp_session.cpp b/src/ssh/plain_sftp_session.cpp new file mode 100644 index 0000000000..406b40456e --- /dev/null +++ b/src/ssh/plain_sftp_session.cpp @@ -0,0 +1,108 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +#include +#include + +#include + +#include +#include +#include + +extern "C" +{ +int sftp_reply_version(sftp_client_message msg); +} + +namespace mp = multipass; + +namespace +{ +using namespace std::literals::chrono_literals; + +void check_sshfs_status(mp::SSHProcess& sshfs_process) +{ + if (sshfs_process.exit_recognized(250ms)) + { + // This `if` is artificial and should not really be here. However there is a complex + // arrangement of Sftp and SshfsMount tests depending on this. + // TODO@sftp no longer needed - just write new tests properly + if (sshfs_process.exit_code(250ms) != 0) // TODO remove + throw std::runtime_error(sshfs_process.read_std_error()); + } +} + +auto create_sshfs_process(mp::PlainSSHSession& session, const std::string& sshfs_cmd) +{ + auto sshfs_process = session.exec_plain(sshfs_cmd); + + check_sshfs_status(*sshfs_process); + + return sshfs_process; +} +} // namespace + +mp::PlainSftpSession::RawSftpSessionUptr +mp::PlainSftpSession::make_raw_sftp_session(ssh_session raw_session, ssh_channel channel) +{ + // The function sftp_server_init was expanded here to avoid deprecation warnings. + // TODO: move to callback-based sftp implementations. + // https://github.com/canonical/multipass/issues/4445 + + // TODO@sftp go through MP_LIBSSH + RawSftpSessionUptr raw_sftp_session{sftp_server_new(raw_session, channel), sftp_server_free}; + if (!raw_sftp_session) + throw SSHException( + fmt::format("[sftp] server init failed: could not create a new sftp_server.")); + + /* handles setting the sftp->client_version */ + // TODO@sftp no leak plz - use SftpMessage + sftp_client_message msg{sftp_get_client_message(raw_sftp_session.get())}; + if (msg == nullptr) + { + throw mp::SSHException("[sftp] server init failed: 'Null client message'"); + } + + if (msg->type != SSH_FXP_INIT) + { + throw mp::SSHException(fmt::format( + "[sftp] server init failed: 'FATAL: Packet read of type {} instead of SSH_FXP_INIT'", + msg->type)); + } + + // Optional: Log the SSH_FXP_INIT reception like libssh does with SSH_LOG but with mp::log + + if (sftp_reply_version(msg) != SSH_OK) + { + throw mp::SSHException( + "[sftp] server init failed: 'FATAL: Failed to process the SSH_FXP_INIT message'"); + } + + return raw_sftp_session; +} + +mp::PlainSftpSession::PlainSftpSession(PlainSSHSession&& ssh_session_obj, + const std::string& sshfs_cmd) + : plain_ssh_session{std::move(ssh_session_obj)}, + sshfs_process{create_sshfs_process(plain_ssh_session, sshfs_cmd)}, + raw_sftp_session{make_raw_sftp_session(plain_ssh_session.borrow_session(pass), + sshfs_process->borrow_channel(pass))} +{ +} diff --git a/src/ssh/plain_ssh_process.cpp b/src/ssh/plain_ssh_process.cpp index 459a2886e7..e0e4fcc69e 100644 --- a/src/ssh/plain_ssh_process.cpp +++ b/src/ssh/plain_ssh_process.cpp @@ -44,7 +44,7 @@ class ExitStatusCallback public: ExitStatusCallback(ssh_channel channel, T& result_holder) : channel{channel} { - ssh_callbacks_init(&cb); + MP_LIBSSH.ssh_callbacks_initialize(&cb); cb.channel_exit_status_function = channel_exit_status_cb; cb.userdata = &result_holder; registered = MP_LIBSSH.ssh_add_channel_callbacks(channel, &cb); @@ -70,24 +70,24 @@ class ExitStatusCallback int registered{}; }; -auto make_channel(ssh_session session, const std::string& cmd) +auto make_channel(ssh_session raw_session, const std::string& cmd) { - if (!MP_LIBSSH.ssh_is_connected(session)) + if (!MP_LIBSSH.ssh_is_connected(raw_session)) throw mp::SSHException(fmt::format( "unable to create a channel for remote process: '{}', the SSH session is not connected", cmd)); mp::PlainSSHProcess::ChannelUPtr channel{ - MP_LIBSSH.ssh_channel_new(session), + MP_LIBSSH.ssh_channel_new(raw_session), [](ssh_channel ch) { MP_LIBSSH.ssh_channel_free(ch); }}; mp::SSH::throw_on_error( channel, - session, + raw_session, "[ssh proc] failed to open session channel", std::bind_front(&mp::Libssh::ssh_channel_open_session, &mp::Libssh::instance())); mp::SSH::throw_on_error( channel, - session, + raw_session, "[ssh proc] exec request failed", std::bind_front(&mp::Libssh::ssh_channel_request_exec, &mp::Libssh::instance()), cmd.c_str()); @@ -96,14 +96,14 @@ auto make_channel(ssh_session session, const std::string& cmd) } // namespace -mp::PlainSSHProcess::PlainSSHProcess(ssh_session_struct& session, +mp::PlainSSHProcess::PlainSSHProcess(ssh_session_struct& raw_session, const std::string& cmd, std::unique_lock session_lock) : session_lock{std::move( session_lock)}, // this is held until the exit code is requested or this is destroyed - session{&session}, + raw_session{&raw_session}, cmd{cmd}, - channel{make_channel(this->session, cmd)}, + channel{make_channel(this->raw_session, cmd)}, exit_result{} { assert(this->session_lock.owns_lock()); @@ -162,9 +162,9 @@ void mp::PlainSSHProcess::read_exit_code(std::chrono::milliseconds timeout, bool err = "could not allocate event"; else if (!cb.is_registered()) err = "could not register callback"; - else if ((MP_LIBSSH.ssh_event_add_session(event.get(), session) != SSH_OK)) + else if ((MP_LIBSSH.ssh_event_add_session(event.get(), raw_session) != SSH_OK)) { - const auto raw_err = MP_LIBSSH.ssh_get_error(session); + const auto raw_err = MP_LIBSSH.ssh_get_error(raw_session); err = fmt::format("could not add event to session: {}", raw_err && *raw_err ? raw_err : "Empty error"); } @@ -263,7 +263,14 @@ std::string mp::PlainSSHProcess::read_stream(StreamType type, int timeout) return output.str(); } -ssh_channel mp::PlainSSHProcess::release_channel() +ssh_channel mp::PlainSSHProcess::borrow_channel( + const PrivatePassProvider::PrivatePass&) +{ + auto local_lock = std::move(session_lock); // released at end; caller gets a non-owning handle + return channel.get(); +} + +ssh_channel mp::PlainSSHProcess::release_channel() // TODO@sftp remove entirely { // released at the end; callers are on their own to ensure thread safety auto local_lock = std::move(session_lock); diff --git a/src/ssh/plain_ssh_session.cpp b/src/ssh/plain_ssh_session.cpp index af3b549c90..98e1c63f56 100644 --- a/src/ssh/plain_ssh_session.cpp +++ b/src/ssh/plain_ssh_session.cpp @@ -15,13 +15,16 @@ * */ +#include + #include #include #include #include #include #include -#include +#include +#include #include #include #include @@ -44,9 +47,9 @@ mp::PlainSSHSession::PlainSSHSession(const std::string& host, int port, const std::string& username, const SSHKeyProvider& key_provider) - : session{MP_LIBSSH.ssh_new(), [](ssh_session s) { MP_LIBSSH.ssh_free(s); }}, mut{} + : raw_session{MP_LIBSSH.ssh_new(), [](ssh_session s) { MP_LIBSSH.ssh_free(s); }}, mut{} { - if (session == nullptr) + if (raw_session == nullptr) throw mp::SSHException("could not allocate ssh session"); /** @@ -75,49 +78,47 @@ mp::PlainSSHSession::PlainSSHSession(const std::string& host, set_option(SSH_OPTIONS_CIPHERS_S_C, "chacha20-poly1305@openssh.com,aes256-ctr"); set_option(SSH_OPTIONS_SSH_DIR, ssh_dir.c_str()); - SSH::throw_on_error(session, + SSH::throw_on_error(raw_session, "ssh connection failed", std::bind_front(&Libssh::ssh_connect, &Libssh::instance())); set_option(SSH_OPTIONS_TIMEOUT, &established_timeout_secs); - SSH::throw_on_error(session, + SSH::throw_on_error(raw_session, "ssh failed to authenticate", std::bind_front(&Libssh::ssh_userauth_publickey, &Libssh::instance()), nullptr, key_provider.private_key()); } -multipass::PlainSSHSession::PlainSSHSession(multipass::PlainSSHSession&& other) +mp::PlainSSHSession::PlainSSHSession(PlainSSHSession&& other) : PlainSSHSession(std::move(other), std::unique_lock{other.mut}) { } -multipass::PlainSSHSession::PlainSSHSession(multipass::PlainSSHSession&& other, - std::unique_lock) - : session{std::move(other.session)}, mut{} +mp::PlainSSHSession::PlainSSHSession(PlainSSHSession&& other, std::unique_lock) + : raw_session{std::move(other.raw_session)}, mut{} { } -multipass::PlainSSHSession& multipass::PlainSSHSession::operator=( - multipass::PlainSSHSession&& other) +mp::PlainSSHSession& mp::PlainSSHSession::operator=(PlainSSHSession&& other) { if (this != &other) { std::scoped_lock lock{mut, other.mut}; - session = std::move(other.session); + raw_session = std::move(other.raw_session); } return *this; } -multipass::PlainSSHSession::~PlainSSHSession() +mp::PlainSSHSession::~PlainSSHSession() { top_catch_all(category, [this] { std::unique_lock lock{mut}; - if (session) + if (raw_session) { mpl::trace(category, "disconnecting SSH session"); - MP_LIBSSH.ssh_disconnect(session.get()); + MP_LIBSSH.ssh_disconnect(raw_session.get()); PlainSSHSession::force_shutdown(); // Shutdown I/O on manually open sockets. // The socket is still closed by libssh in ssh_free. } @@ -125,6 +126,12 @@ multipass::PlainSSHSession::~PlainSSHSession() } std::unique_ptr mp::PlainSSHSession::exec(const std::string& cmd, bool whisper) +{ + return exec_plain(cmd, whisper); +} + +std::unique_ptr mp::PlainSSHSession::exec_plain(const std::string& cmd, + bool whisper) { std::unique_lock lock{mut}; assert(!is_moved() && "precondition - cannot call exec on a moved session"); @@ -132,7 +139,13 @@ std::unique_ptr mp::PlainSSHSession::exec(const std::string& cmd auto lvl = whisper ? mpl::Level::trace : mpl::Level::debug; mpl::log(lvl, category, "Executing '{}'", cmd); - return std::make_unique(*session.get(), cmd, std::move(lock)); + return std::make_unique(*raw_session.get(), cmd, std::move(lock)); +} + +std::unique_ptr mp::PlainSSHSession::make_sftp_session( + const std::string& sshfs_cmd) && +{ + return std::make_unique(std::move(*this), sshfs_cmd); } void mp::PlainSSHSession::force_shutdown() @@ -140,16 +153,23 @@ void mp::PlainSSHSession::force_shutdown() // TODO@sftp This is public but doesn't lock (it can't, because it is also called internally // with a lock acquired). Make it private instead. Provide public way to close the session // (probably just the dtor - let outside callers delete and deal with null session) - if (!session) + if (!raw_session) return; - if (auto socket = MP_LIBSSH.ssh_get_fd(session.get()); socket != -1) + if (auto socket = MP_LIBSSH.ssh_get_fd(raw_session.get()); socket != -1) MP_PLATFORM.shutdown_socket(socket); } +ssh_session multipass::PlainSSHSession::borrow_session( + const PrivatePassProvider::PrivatePass&) const +{ + assert(!is_moved() && "precondition - cannot borrow a moved session"); + return raw_session.get(); +} + mp::PlainSSHSession::operator ssh_session() { - return session.get(); + return raw_session.get(); } namespace @@ -206,25 +226,25 @@ std::string as_string(ssh_options_e type, const void* value) void mp::PlainSSHSession::set_option(ssh_options_e type, const void* data) { std::unique_lock lock{mut}; - assert(session && "should not set option on null session"); + assert(raw_session && "should not set option on null session"); - const auto ret = MP_LIBSSH.ssh_options_set(session.get(), type, data); + const auto ret = MP_LIBSSH.ssh_options_set(raw_session.get(), type, data); if (ret != SSH_OK) { throw mp::SSHException(fmt::format("libssh failed to set {} option to '{}': '{}'", name_for(type), as_string(type, data), - MP_LIBSSH.ssh_get_error(session.get()))); + MP_LIBSSH.ssh_get_error(raw_session.get()))); } } bool mp::PlainSSHSession::is_connected() const { std::unique_lock lock{mut}; - return session && static_cast(MP_LIBSSH.ssh_is_connected(session.get())); + return raw_session && static_cast(MP_LIBSSH.ssh_is_connected(raw_session.get())); } bool mp::PlainSSHSession::is_moved() const { - return !session; + return !raw_session; } diff --git a/src/ssh/sftp_client.cpp b/src/ssh/sftp_client.cpp index 5ecdf2d8f5..bbd8201ed9 100644 --- a/src/ssh/sftp_client.cpp +++ b/src/ssh/sftp_client.cpp @@ -61,11 +61,11 @@ SFTPClient::SFTPClient(const std::string& host, { } -SFTPClient::SFTPClient(SSHSessionUPtr ssh_session) - : ssh_session{std::move(ssh_session)}, sftp{make_sftp_session(*this->ssh_session)} +SFTPClient::SFTPClient(SSHSessionUPtr ssh_session_obj) + : ssh_session_obj{std::move(ssh_session_obj)}, sftp{make_sftp_session(*this->ssh_session_obj)} { SSH::throw_on_error(sftp, - *this->ssh_session, + *this->ssh_session_obj, "[sftp] init failed", std::bind_front(&Libssh::sftp_init, &Libssh::instance())); } diff --git a/src/ssh/ssh_client.cpp b/src/ssh/ssh_client.cpp index bc628d620f..b6bb374875 100644 --- a/src/ssh/ssh_client.cpp +++ b/src/ssh/ssh_client.cpp @@ -71,9 +71,9 @@ mp::SSHClient::SSHClient(const std::string& host, { } -mp::SSHClient::SSHClient(SSHSessionUPtr ssh_session, ConsoleCreator console_creator) - : ssh_session{std::move(ssh_session)}, - channel{make_channel(*this->ssh_session)}, +mp::SSHClient::SSHClient(SSHSessionUPtr ssh_session_obj, ConsoleCreator console_creator) + : ssh_session_obj{std::move(ssh_session_obj)}, + channel{make_channel(*this->ssh_session_obj)}, console{console_creator(channel.get())} { } @@ -107,7 +107,7 @@ void mp::SSHClient::handle_ssh_events() [](ssh_event e) { MP_LIBSSH.ssh_event_free(e); }}; // stdin - ConnectorUPtr connector_in{MP_LIBSSH.ssh_connector_new(*ssh_session), + ConnectorUPtr connector_in{MP_LIBSSH.ssh_connector_new(*ssh_session_obj), [](ssh_connector c) { MP_LIBSSH.ssh_connector_free(c); }}; MP_LIBSSH.ssh_connector_set_out_channel(connector_in.get(), channel.get(), @@ -116,7 +116,7 @@ void mp::SSHClient::handle_ssh_events() MP_LIBSSH.ssh_event_add_connector(event.get(), connector_in.get()); // stdout - ConnectorUPtr connector_out{MP_LIBSSH.ssh_connector_new(*ssh_session), + ConnectorUPtr connector_out{MP_LIBSSH.ssh_connector_new(*ssh_session_obj), [](ssh_connector c) { MP_LIBSSH.ssh_connector_free(c); }}; MP_LIBSSH.ssh_connector_set_out_fd(connector_out.get(), fileno(stdout)); MP_LIBSSH.ssh_connector_set_in_channel(connector_out.get(), @@ -125,7 +125,7 @@ void mp::SSHClient::handle_ssh_events() MP_LIBSSH.ssh_event_add_connector(event.get(), connector_out.get()); // stderr - ConnectorUPtr connector_err{MP_LIBSSH.ssh_connector_new(*ssh_session), + ConnectorUPtr connector_err{MP_LIBSSH.ssh_connector_new(*ssh_session_obj), [](ssh_connector c) { MP_LIBSSH.ssh_connector_free(c); }}; MP_LIBSSH.ssh_connector_set_out_fd(connector_err.get(), fileno(stderr)); MP_LIBSSH.ssh_connector_set_in_channel(connector_err.get(), @@ -169,12 +169,12 @@ int mp::SSHClient::exec_string(const std::string& cmd_line) if (cmd_line.empty()) SSH::throw_on_error( channel, - *ssh_session, + *ssh_session_obj, "[ssh client] shell request failed", std::bind_front(&Libssh::ssh_channel_request_shell, &Libssh::instance())); else SSH::throw_on_error(channel, - *ssh_session, + *ssh_session_obj, "[ssh client] exec request failed", std::bind_front(&Libssh::ssh_channel_request_exec, &Libssh::instance()), cmd_line.c_str()); @@ -191,7 +191,7 @@ int mp::SSHClient::get_ssh_exit_code() int core_dumped = 0; SSH::throw_on_error(channel, - *ssh_session, + *ssh_session_obj, "[ssh client] could not obtain exit state", std::bind_front(&Libssh::ssh_channel_get_exit_state, &Libssh::instance()), &exit_status, diff --git a/src/sshfs_mount/CMakeLists.txt b/src/sshfs_mount/CMakeLists.txt index 634afdeb7e..35f6f31903 100644 --- a/src/sshfs_mount/CMakeLists.txt +++ b/src/sshfs_mount/CMakeLists.txt @@ -14,8 +14,6 @@ set(CMAKE_AUTOMOC ON) function(add_sshfs_mount_target TARGET_NAME LIBSSH_TARGET) - add_definitions(-DWITH_SERVER) - add_library(${TARGET_NAME} STATIC sshfs_mount.cpp sshfs_mount_handler.cpp @@ -24,6 +22,8 @@ function(add_sshfs_mount_target TARGET_NAME LIBSSH_TARGET) sshfs_mount.h ${CMAKE_SOURCE_DIR}/include/multipass/sshfs_mount/sshfs_mount_handler.h) + target_compile_definitions(${TARGET_NAME} PRIVATE WITH_SERVER) + target_link_libraries(${TARGET_NAME} ${LIBSSH_TARGET} fmt::fmt-header-only diff --git a/src/sshfs_mount/sftp_server.cpp b/src/sshfs_mount/sftp_server.cpp index 44919f9362..772510df23 100644 --- a/src/sshfs_mount/sftp_server.cpp +++ b/src/sshfs_mount/sftp_server.cpp @@ -55,6 +55,7 @@ enum Permissions exec_other = 01 }; +// TODO@sftp dump auto make_sftp_session(ssh_session session, ssh_channel channel) { mp::SftpServer::SftpSessionUptr sftp_server_session{ @@ -220,6 +221,7 @@ auto to_unix_permissions(QFile::Permissions perms) return out; } +// TODO@sftp dump void check_sshfs_status(mp::SSHProcess& sshfs_process) { if (sshfs_process.exit_recognized(250ms)) @@ -231,6 +233,7 @@ void check_sshfs_status(mp::SSHProcess& sshfs_process) } } +// TODO@sftp dump auto create_sshfs_process(mp::SSHSession& session, const std::string& sshfs_exec_line, const std::string& source, @@ -304,11 +307,11 @@ mp::SftpServer::SftpServer(std::unique_ptr&& session, int default_uid, int default_gid, const std::string& sshfs_exec_line) - : ssh_session{std::move(session)}, - sshfs_process{create_sshfs_process(*ssh_session, sshfs_exec_line, source, target)}, - sftp_server_session{make_sftp_session(*ssh_session, - static_cast(sshfs_process.get()) - ->release_channel())}, // TODO@rewiressh no cast + : ssh_session_obj{std::move(session)}, // TODO@sftp dump field + sshfs_process{create_sshfs_process(*ssh_session_obj, sshfs_exec_line, source, target)}, + raw_sftp_session{make_sftp_session(*ssh_session_obj, + static_cast(sshfs_process.get()) + ->release_channel())}, // TODO@rewiressh no cast source_path{MP_FILEOPS.weakly_canonical(source)}, target_path{fs::path(target).lexically_normal()}, gid_mappings{gid_mappings}, @@ -566,7 +569,7 @@ void mp::SftpServer::run() while (true) { - MsgUPtr client_msg{MP_LIBSSH.sftp_get_client_message(sftp_server_session.get()), + MsgUPtr client_msg{MP_LIBSSH.sftp_get_client_message(raw_sftp_session.get()), [](sftp_client_message m) { MP_LIBSSH.sftp_client_message_free(m); }}; auto msg = client_msg.get(); if (msg == nullptr) @@ -592,7 +595,7 @@ void mp::SftpServer::run() "recover."); std::string mount_path = [this] { - auto proc = ssh_session->exec( + auto proc = ssh_session_obj->exec( fmt::format("findmnt --source :{} -o TARGET -n", source_path)); return proc->read_std_output(); }(); @@ -600,17 +603,17 @@ void mp::SftpServer::run() if (!mount_path.empty()) { // TODO@sftp nodiscard - (void)ssh_session->exec(fmt::format("sudo umount {}", mount_path)); + (void)ssh_session_obj->exec(fmt::format("sudo umount {}", mount_path)); } - sshfs_process = create_sshfs_process(*ssh_session, + sshfs_process = create_sshfs_process(*ssh_session_obj, sshfs_exec_line, source_path.string(), target_path.generic_string()); - sftp_server_session = - make_sftp_session(*ssh_session, - static_cast(sshfs_process.get()) - ->release_channel()); // TODO@rewiressh no cast + raw_sftp_session = make_sftp_session( + *ssh_session_obj, + static_cast(sshfs_process.get()) + ->release_channel()); // TODO@rewiressh no cast continue; } @@ -627,19 +630,19 @@ void mp::SftpServer::run() void mp::SftpServer::stop() { stop_invoked = true; - ssh_session->force_shutdown(); // TODO@sftp there should be a better way... + ssh_session_obj->force_shutdown(); // TODO@sftp there should be a better way... } int mp::SftpServer::handle_close(sftp_client_message msg) { - const auto id = MP_LIBSSH.sftp_handle(sftp_server_session.get(), msg->handle); + const auto id = MP_LIBSSH.sftp_handle(raw_sftp_session.get(), msg->handle); if (!open_file_handles.erase(id) && !open_dir_handles.erase(id)) { mpl::trace(category, "{}: bad handle requested", __FUNCTION__); return reply_bad_handle(msg, "close"); } - MP_LIBSSH.sftp_handle_remove(sftp_server_session.get(), id); + MP_LIBSSH.sftp_handle_remove(raw_sftp_session.get(), id); return reply_ok(msg); } @@ -836,9 +839,8 @@ int mp::SftpServer::handle_open(sftp_client_message msg) } } - SftpHandleUPtr sftp_handle{ - MP_LIBSSH.sftp_handle_alloc(sftp_server_session.get(), named_fd.get()), - [](ssh_string s) { MP_LIBSSH.ssh_string_free(s); }}; + SftpHandleUPtr sftp_handle{MP_LIBSSH.sftp_handle_alloc(raw_sftp_session.get(), named_fd.get()), + [](ssh_string s) { MP_LIBSSH.ssh_string_free(s); }}; if (!sftp_handle) { mpl::trace(category, "Cannot allocate handle for open()"); @@ -883,7 +885,7 @@ int mp::SftpServer::handle_opendir(sftp_client_message msg) } SftpHandleUPtr sftp_handle{ - MP_LIBSSH.sftp_handle_alloc(sftp_server_session.get(), dir_iterator.get()), + MP_LIBSSH.sftp_handle_alloc(raw_sftp_session.get(), dir_iterator.get()), [](ssh_string s) { MP_LIBSSH.ssh_string_free(s); }}; if (!sftp_handle) { diff --git a/src/sshfs_mount/sftp_server.h b/src/sshfs_mount/sftp_server.h index f3d2659fa4..a6f002e093 100644 --- a/src/sshfs_mount/sftp_server.h +++ b/src/sshfs_mount/sftp_server.h @@ -32,12 +32,12 @@ namespace multipass { class SSHSession; -class SSHProcess; +class SSHProcess; // TODO@sftp remove class SftpServer { public: - SftpServer(std::unique_ptr&& ssh_session, + SftpServer(std::unique_ptr&& ssh_session_obj, const std::string& source, const std::string& target, const id_mappings& gid_mappings, @@ -51,6 +51,7 @@ class SftpServer void run(); void stop(); + // TODO@sftp remove using SSHSessionUptr = std::unique_ptr; using SftpSessionUptr = std::unique_ptr; using SSHFSProcUptr = std::unique_ptr; @@ -93,9 +94,9 @@ class SftpServer template T* get_handle(sftp_client_message msg); - std::unique_ptr ssh_session; - SSHFSProcUptr sshfs_process; - SftpSessionUptr sftp_server_session; + std::unique_ptr ssh_session_obj; // TODO@sftp remove + SSHFSProcUptr sshfs_process; // TODO@sftp remove + SftpSessionUptr raw_sftp_session; // TODO@sftp remove const std::filesystem::path source_path; const std::filesystem::path target_path; std::unordered_map> open_file_handles; diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 3bdb768160..af28042f38 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -110,7 +110,9 @@ add_executable(multipass_cpp_tests test_output_formatter.cpp test_permission_utils.cpp test_persistent_settings_handler.cpp + test_plain_sftp_session.cpp test_plain_ssh_session.cpp + test_plain_ssh_session_mocked_libssh.cpp test_private_pass_provider.cpp test_qemu_img_utils.cpp test_qemuimg_process_spec.cpp @@ -153,9 +155,8 @@ target_include_directories(multipass_cpp_tests PRIVATE ${CMAKE_SOURCE_DIR}/src/platform/backends ) -add_definitions(-DWITH_SERVER) -target_compile_definitions(libssh_wrapper_test PRIVATE - ${c_mock_defines}) +target_compile_definitions(multipass_cpp_tests PRIVATE WITH_SERVER) +target_compile_definitions(libssh_wrapper_test PRIVATE ${c_mock_defines}) target_compile_definitions(utils_test PRIVATE -DEVP_PBE_scrypt=ut_premock_EVP_PBE_scrypt) diff --git a/tests/unit/mock_libssh.h b/tests/unit/mock_libssh.h index b3ca3cf568..710c0c5f32 100644 --- a/tests/unit/mock_libssh.h +++ b/tests/unit/mock_libssh.h @@ -90,6 +90,7 @@ class MockLibssh : public Libssh (const, override)); // --- channel callbacks --------------------------------------------------- + MOCK_METHOD(void, ssh_callbacks_initialize, (ssh_channel_callbacks cb), (const, override)); MOCK_METHOD(int, ssh_add_channel_callbacks, (ssh_channel channel, ssh_channel_callbacks cb), diff --git a/tests/unit/mock_ssh_session.h b/tests/unit/mock_ssh_session.h index 00334bf60e..a2a6f69a7d 100644 --- a/tests/unit/mock_ssh_session.h +++ b/tests/unit/mock_ssh_session.h @@ -21,6 +21,7 @@ #include "mock_ssh_process.h" #include +#include namespace multipass::test { @@ -42,6 +43,11 @@ struct MockSSHSession : public SSHSession { return nullptr; } + MOCK_METHOD(void, force_shutdown, (), (override)); + MOCK_METHOD(std::unique_ptr, + make_sftp_session, + (const std::string& sshfs_cmd), + (ref(&&), override)); }; } // namespace multipass::test diff --git a/tests/unit/test_plain_sftp_session.cpp b/tests/unit/test_plain_sftp_session.cpp new file mode 100644 index 0000000000..0f6b154028 --- /dev/null +++ b/tests/unit/test_plain_sftp_session.cpp @@ -0,0 +1,148 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "common.h" +#include "mock_libssh.h" +#include "stub_ssh_key_provider.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace mp = multipass; +namespace mpt = multipass::test; +using namespace testing; + +namespace +{ +static_assert(!std::is_default_constructible_v, "only for derived classes"); +static_assert(std::has_virtual_destructor_v); +static_assert(!std::is_copy_constructible_v); +static_assert(!std::is_copy_assignable_v); +static_assert(!std::is_copy_constructible_v); +static_assert(!std::is_copy_assignable_v); +static_assert(!std::is_move_constructible_v); +static_assert(!std::is_move_assignable_v); + +// make_sftp_session consumes SSHSession +using MakeSftpSession = decltype(&mp::SSHSession::make_sftp_session); +static_assert(!std::is_invocable_v, + "make_sftp_session must consume the session (callable only on an rvalue)"); +static_assert(std::is_invocable_v); + +struct TestPlainSftpSession : public Test +{ + TestPlainSftpSession() + { + ON_CALL(mock_libssh, ssh_new()).WillByDefault(Return(fake_session)); + ON_CALL(mock_libssh, ssh_options_set).WillByDefault(Return(SSH_OK)); + ON_CALL(mock_libssh, ssh_connect).WillByDefault(Return(SSH_OK)); + ON_CALL(mock_libssh, ssh_userauth_publickey).WillByDefault(Return(SSH_AUTH_SUCCESS)); + ON_CALL(mock_libssh, ssh_is_connected).WillByDefault(Return(1)); + ON_CALL(mock_libssh, ssh_channel_new).WillByDefault(Return(fake_channel)); + ON_CALL(mock_libssh, ssh_channel_open_session).WillByDefault(Return(SSH_OK)); + ON_CALL(mock_libssh, ssh_channel_request_exec).WillByDefault(Return(SSH_OK)); + ON_CALL(mock_libssh, ssh_get_fd).WillByDefault(Return(-1)); // no socket to shutdown + ON_CALL(mock_libssh, ssh_get_error).WillByDefault(Return("mocked error")); + + // Exit-status machinery: deliver `sshfs_exit_code` through the registered callback when + // the event loop polls, as libssh would on a channel-exit-status message + ON_CALL(mock_libssh, ssh_add_channel_callbacks) + .WillByDefault(DoAll(SaveArg<1>(&channel_cbs), Return(SSH_OK))); + ON_CALL(mock_libssh, ssh_remove_channel_callbacks).WillByDefault(Return(SSH_OK)); + ON_CALL(mock_libssh, ssh_event_new()).WillByDefault(Return(fake_event)); + ON_CALL(mock_libssh, ssh_event_add_session).WillByDefault(Return(SSH_OK)); + ON_CALL(mock_libssh, ssh_event_dopoll).WillByDefault([this](ssh_event, int) { + channel_cbs->channel_exit_status_function(fake_session, + fake_channel, + sshfs_exit_code, + channel_cbs->userdata); + return SSH_OK; + }); + } + + mp::PlainSSHSession make_ssh_session() const + { + return mp::PlainSSHSession{"host", 42, "ubuntu", key_provider}; + } + + mpt::MockLibssh::GuardedMock guarded_mock = mpt::MockLibssh::inject(); + mpt::MockLibssh& mock_libssh = *guarded_mock.first; + + constexpr static auto bad_addr = 0xdeadbeefdeadbeefull; // should reliably segfault on 32/64-bit + ssh_session fake_session = reinterpret_cast(bad_addr); + ssh_channel fake_channel = reinterpret_cast(bad_addr); + ssh_event fake_event = reinterpret_cast(bad_addr); + + ssh_channel_callbacks channel_cbs = nullptr; + int sshfs_exit_code = 0; + + mpt::StubSSHKeyProvider key_provider; +}; +} // namespace + +TEST_F(TestPlainSftpSession, makeSftpSessionRunsSshfsCommand) +{ + sshfs_exit_code = 1; // TODO@sftp mock success path instead + + auto session = make_ssh_session(); + EXPECT_CALL(mock_libssh, ssh_channel_request_exec(fake_channel, StrEq("sshfs -o slave"))) + .WillOnce(Return(SSH_OK)); + + EXPECT_ANY_THROW(static_cast(std::move(session).make_sftp_session("sshfs -o slave"))); +} + +TEST_F(TestPlainSftpSession, makeSftpSessionThrowsSshfsErrorWhenSshfsFails) +{ + sshfs_exit_code = 127; + const std::string error = "sshfs bonkers"; + EXPECT_CALL(mock_libssh, ssh_channel_read_timeout).WillRepeatedly(Return(0)); + EXPECT_CALL(mock_libssh, ssh_channel_read_timeout(_, _, _, Ne(0), _)) + .WillOnce(WithArgs<1, 2>([&error](void* dest, uint32_t count) { + const auto num_bytes = std::min(error.size(), count); + std::memcpy(dest, error.data(), num_bytes); + return static_cast(num_bytes); + })) + .RetiresOnSaturation(); + + auto session = make_ssh_session(); + + MP_EXPECT_THROW_THAT(static_cast(std::move(session).make_sftp_session("sshfs")), + std::runtime_error, + mpt::match_what(StrEq(error))); +} + +TEST_F(TestPlainSftpSession, releasesConsumedSessionOnce) +{ + sshfs_exit_code = 127; + + auto session = make_ssh_session(); + { + EXPECT_CALL(mock_libssh, ssh_channel_free(fake_channel)).Times(1); + EXPECT_CALL(mock_libssh, ssh_free(fake_session)).Times(1); + + EXPECT_ANY_THROW(static_cast(std::move(session).make_sftp_session("sshfs"))); + } // session internals freed + + EXPECT_TRUE(session.is_moved()); +} diff --git a/tests/unit/test_plain_ssh_session_mocked_libssh.cpp b/tests/unit/test_plain_ssh_session_mocked_libssh.cpp new file mode 100644 index 0000000000..0d4af963d3 --- /dev/null +++ b/tests/unit/test_plain_ssh_session_mocked_libssh.cpp @@ -0,0 +1,146 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "common.h" +#include "mock_libssh.h" +#include "stub_ssh_key_provider.h" + +#include +#include +#include + +#include +#include + +namespace mp = multipass; +namespace mpt = multipass::test; +using namespace testing; + +namespace +{ +static_assert(std::is_final_v, "required to prevent chopping on move"); +static_assert(!std::is_copy_constructible_v); +static_assert(!std::is_copy_assignable_v); +static_assert(std::is_move_constructible_v); +static_assert(std::is_move_assignable_v); +static_assert(!std::is_copy_constructible_v); +static_assert(!std::is_copy_assignable_v); + +// TODO@sftp transfer premock-based session tests to this scheme; then, rename this file/suite +struct TestPlainSSHSessionMockedLibssh : public Test +{ + TestPlainSSHSessionMockedLibssh() + { + ON_CALL(mock_libssh, ssh_new()).WillByDefault(Return(fake_session)); + ON_CALL(mock_libssh, ssh_options_set).WillByDefault(Return(SSH_OK)); + ON_CALL(mock_libssh, ssh_connect).WillByDefault(Return(SSH_OK)); + ON_CALL(mock_libssh, ssh_userauth_publickey).WillByDefault(Return(SSH_AUTH_SUCCESS)); + ON_CALL(mock_libssh, ssh_is_connected).WillByDefault(Return(1)); + ON_CALL(mock_libssh, ssh_channel_new).WillByDefault(Return(fake_channel)); + ON_CALL(mock_libssh, ssh_channel_open_session).WillByDefault(Return(SSH_OK)); + ON_CALL(mock_libssh, ssh_channel_request_exec).WillByDefault(Return(SSH_OK)); + ON_CALL(mock_libssh, ssh_get_fd).WillByDefault(Return(-1)); // no socket to shutdown + ON_CALL(mock_libssh, ssh_get_error).WillByDefault(Return("mocked error")); + } + + mp::PlainSSHSession make_ssh_session() const + { + return mp::PlainSSHSession{"host", 42, "ubuntu", key_provider}; + } + + mpt::MockLibssh::GuardedMock guarded_mock = mpt::MockLibssh::inject(); + mpt::MockLibssh& mock_libssh = *guarded_mock.first; + + constexpr static auto bad_addr = 0xdeadbeefdeadbeefull; // should reliably segfault on 32/64-bit + constexpr static auto bad_addr_too = 0xbadadd4f0ccac1adull; // idem + ssh_session fake_session = reinterpret_cast(bad_addr); + ssh_channel fake_channel = reinterpret_cast(bad_addr); + + mpt::StubSSHKeyProvider key_provider; +}; +} // namespace + +TEST_F(TestPlainSSHSessionMockedLibssh, execPlainReturnsConcreteProcessRunningGivenCommand) +{ + auto session = make_ssh_session(); + EXPECT_CALL(mock_libssh, ssh_channel_request_exec(fake_channel, StrEq("ls -la"))) + .WillOnce(Return(SSH_OK)); + + std::unique_ptr proc = session.exec_plain("ls -la"); + + ASSERT_THAT(proc, NotNull()); + EXPECT_EQ(proc->get_cmd(), "ls -la"); +} + +TEST_F(TestPlainSSHSessionMockedLibssh, execPlainThrowsOnDisconnectedSession) +{ + auto session = make_ssh_session(); + EXPECT_CALL(mock_libssh, ssh_is_connected(fake_session)).WillOnce(Return(0)); + + MP_EXPECT_THROW_THAT(static_cast(session.exec_plain("cmd")), + mp::SSHException, + mpt::match_what(HasSubstr("not connected"))); +} + +TEST_F(TestPlainSSHSessionMockedLibssh, execProducesPlainProcess) +{ + auto session = make_ssh_session(); + + auto proc = session.exec("true"); + + ASSERT_THAT(proc, NotNull()); + EXPECT_THAT(dynamic_cast(proc.get()), NotNull()); +} + +TEST_F(TestPlainSSHSessionMockedLibssh, moveConstructionLeavesSourceMoved) +{ + auto session1 = make_ssh_session(); + EXPECT_FALSE(session1.is_moved()); + + auto session2 = std::move(session1); + + EXPECT_TRUE(session1.is_moved()); + EXPECT_FALSE(session2.is_moved()); +} + +TEST_F(TestPlainSSHSessionMockedLibssh, moveAssignmentTransfersUnderlyingSession) +{ + auto other_session = reinterpret_cast(bad_addr_too); + EXPECT_CALL(mock_libssh, ssh_new()) + .WillOnce(Return(fake_session)) + .WillOnce(Return(other_session)); + + auto session1 = make_ssh_session(); + auto session2 = make_ssh_session(); + + session1 = std::move(session2); + + EXPECT_TRUE(session2.is_moved()); + EXPECT_FALSE(session1.is_moved()); + + EXPECT_CALL(mock_libssh, ssh_channel_new(other_session)).WillOnce(Return(fake_channel)); + ASSERT_THAT(session1.exec_plain("cmd"), NotNull()); +} + +TEST_F(TestPlainSSHSessionMockedLibssh, movedSessionReleasesOnce) +{ + EXPECT_CALL(mock_libssh, ssh_free(fake_session)).Times(1); + EXPECT_CALL(mock_libssh, ssh_disconnect(fake_session)).Times(1); + + auto session1 = make_ssh_session(); + auto session2 = std::move(session1); +}