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/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 9b8bbc4468..e0e4fcc69e 100644
--- a/src/ssh/plain_ssh_process.cpp
+++ b/src/ssh/plain_ssh_process.cpp
@@ -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_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);
+}