diff --git a/plugins/remote/include/content/helpers/sftp_client.hpp b/plugins/remote/include/content/helpers/sftp_client.hpp index 1af1e5ca2..3f4f6ea63 100644 --- a/plugins/remote/include/content/helpers/sftp_client.hpp +++ b/plugins/remote/include/content/helpers/sftp_client.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -22,27 +23,27 @@ namespace hex::plugin::remote { - class SFTPClient { + class SSHClient { public: - SFTPClient() = default; - SFTPClient(const std::string &host, + SSHClient() = default; + SSHClient(const std::string &host, int port, const std::string &user, const std::string &password); - SFTPClient(const std::string &host, + SSHClient(const std::string &host, int port, const std::string &user, const std::fs::path &privateKeyPath, const std::string &passphrase); - ~SFTPClient(); + ~SSHClient(); - SFTPClient(const SFTPClient&) = delete; - SFTPClient& operator=(const SFTPClient&) = delete; + SSHClient(const SSHClient&) = delete; + SSHClient& operator=(const SSHClient&) = delete; - SFTPClient(SFTPClient &&other) noexcept; - SFTPClient& operator=(SFTPClient &&other) noexcept; + SSHClient(SSHClient &&other) noexcept; + SSHClient& operator=(SSHClient &&other) noexcept; struct FsItem { std::string name; @@ -66,37 +67,34 @@ namespace hex::plugin::remote { class RemoteFile { public: RemoteFile() = default; - RemoteFile(LIBSSH2_SFTP_HANDLE* handle, OpenMode mode); - ~RemoteFile(); + explicit RemoteFile(OpenMode mode) : m_mode(mode) {} + virtual ~RemoteFile() {} RemoteFile(const RemoteFile&) = delete; RemoteFile& operator=(const RemoteFile&) = delete; - RemoteFile(RemoteFile &&other) noexcept; - RemoteFile& operator=(RemoteFile &&other) noexcept; + RemoteFile(RemoteFile &&other) noexcept = delete; + RemoteFile& operator=(RemoteFile &&other) noexcept = delete; - [[nodiscard]] bool isOpen() const { - return m_handle != nullptr; - } + [[nodiscard]] virtual bool isOpen() const = 0; - [[nodiscard]] size_t read(std::span buffer); - [[nodiscard]] size_t write(std::span buffer); - void seek(uint64_t offset); - [[nodiscard]] u64 tell() const; - [[nodiscard]] u64 size() const; + [[nodiscard]] virtual size_t read(std::span buffer) = 0; + [[nodiscard]] virtual size_t write(std::span buffer) = 0; + virtual void seek(uint64_t offset) = 0; + [[nodiscard]] virtual u64 tell() const = 0; + [[nodiscard]] virtual u64 size() const = 0; - bool eof() const; - void flush(); - void close(); + virtual bool eof() const = 0; + virtual void flush() = 0; + virtual void close() = 0; [[nodiscard]] OpenMode getOpenMode() const { return m_mode; } private: - LIBSSH2_SFTP_HANDLE* m_handle = nullptr; - bool m_atEOF = false; OpenMode m_mode = OpenMode::Read; }; - RemoteFile openFile(const std::fs::path& remotePath, OpenMode mode); + std::unique_ptr openFileSFTP(const std::fs::path& remotePath, OpenMode mode); + std::unique_ptr openFileSSH(const std::fs::path& remotePath, OpenMode mode); void disconnect(); [[nodiscard]] bool isConnected() const { @@ -127,4 +125,62 @@ namespace hex::plugin::remote { std::vector m_cachedFsItems; }; + class RemoteFileSFTP : public SSHClient::RemoteFile { + public: + RemoteFileSFTP() = default; + RemoteFileSFTP(LIBSSH2_SFTP_HANDLE* handle, SSHClient::OpenMode mode); + ~RemoteFileSFTP() override; + + [[nodiscard]] bool isOpen() const override { + return m_handle != nullptr; + } + + [[nodiscard]] size_t read(std::span buffer) override; + [[nodiscard]] size_t write(std::span buffer) override; + void seek(uint64_t offset) override; + [[nodiscard]] u64 tell() const override; + [[nodiscard]] u64 size() const override; + + bool eof() const override; + void flush() override; + void close() override; + + private: + LIBSSH2_SFTP_HANDLE* m_handle = nullptr; + bool m_atEOF = false; + SSHClient::OpenMode m_mode = SSHClient::OpenMode::Read; + }; + + class RemoteFileSSH : public SSHClient::RemoteFile { + public: + RemoteFileSSH() = default; + RemoteFileSSH(LIBSSH2_SESSION* handle, std::string path, SSHClient::OpenMode mode); + ~RemoteFileSSH() override; + + [[nodiscard]] bool isOpen() const override { + return m_handle != nullptr; + } + + [[nodiscard]] size_t read(std::span buffer) override; + [[nodiscard]] size_t write(std::span buffer) override; + void seek(uint64_t offset) override; + [[nodiscard]] u64 tell() const override; + [[nodiscard]] u64 size() const override; + + bool eof() const override; + void flush() override; + void close() override; + + private: + std::vector executeCommand(const std::string &command, std::span writeData = {}) const; + + private: + LIBSSH2_SESSION* m_handle = nullptr; + LIBSSH2_CHANNEL* m_channel = nullptr; + bool m_atEOF = false; + u64 m_seekPosition = 0x00; + std::string m_readCommand, m_writeCommand, m_sizeCommand; + mutable std::mutex m_mutex; + }; + } diff --git a/plugins/remote/include/content/providers/ssh_provider.hpp b/plugins/remote/include/content/providers/ssh_provider.hpp index 9db7cce19..483d3ac85 100644 --- a/plugins/remote/include/content/providers/ssh_provider.hpp +++ b/plugins/remote/include/content/providers/ssh_provider.hpp @@ -9,9 +9,9 @@ namespace hex::plugin::remote { class SSHProvider : public prv::CachedProvider, public prv::IProviderLoadInterface { public: - bool isAvailable() const override { return m_remoteFile.isOpen(); } + bool isAvailable() const override { return m_remoteFile != nullptr && m_remoteFile->isOpen(); } bool isReadable() const override { return isAvailable(); } - bool isWritable() const override { return m_remoteFile.getOpenMode() != SFTPClient::OpenMode::Read; } + bool isWritable() const override { return m_remoteFile != nullptr && m_remoteFile->getOpenMode() != SSHClient::OpenMode::Read; } bool isResizable() const override { return false; } bool isSavable() const override { return isWritable(); } @@ -41,8 +41,8 @@ namespace hex::plugin::remote { }; private: - SFTPClient m_sftpClient; - SFTPClient::RemoteFile m_remoteFile; + SSHClient m_sftpClient; + std::unique_ptr m_remoteFile; std::string m_host; int m_port = 22; @@ -53,6 +53,7 @@ namespace hex::plugin::remote { AuthMethod m_authMethod = AuthMethod::Password; bool m_selectedFile = false; + bool m_accessFileOverSSH = false; std::fs::path m_remoteFilePath = { "/", std::fs::path::format::generic_format }; }; diff --git a/plugins/remote/romfs/lang/en_US.json b/plugins/remote/romfs/lang/en_US.json index 448218eba..186f5d4fe 100644 --- a/plugins/remote/romfs/lang/en_US.json +++ b/plugins/remote/romfs/lang/en_US.json @@ -6,5 +6,6 @@ "hex.plugin.remote.ssh_provider.password": "Password", "hex.plugin.remote.ssh_provider.key_file": "Private Key Path", "hex.plugin.remote.ssh_provider.passphrase": "Passphrase", - "hex.plugin.remote.ssh_provider.connect": "Connect" + "hex.plugin.remote.ssh_provider.connect": "Connect", + "hex.plugin.remote.ssh_provider.ssh_access": "Access file using raw SSH" } diff --git a/plugins/remote/source/content/helpers/sftp_client.cpp b/plugins/remote/source/content/helpers/sftp_client.cpp index f63020f50..c9cda7ab6 100644 --- a/plugins/remote/source/content/helpers/sftp_client.cpp +++ b/plugins/remote/source/content/helpers/sftp_client.cpp @@ -11,15 +11,15 @@ namespace hex::plugin::remote { - void SFTPClient::init() { + void SSHClient::init() { libssh2_init(0); } - void SFTPClient::exit() { + void SSHClient::exit() { libssh2_exit(); } - SFTPClient::SFTPClient(const std::string &host, int port, const std::string &user, const std::string &password) { + SSHClient::SSHClient(const std::string &host, int port, const std::string &user, const std::string &password) { connect(host, port); authenticatePassword(user, password); @@ -28,7 +28,7 @@ namespace hex::plugin::remote { throw std::runtime_error("Failed to initialize SFTP session"); } - SFTPClient::SFTPClient(const std::string &host, int port, const std::string &user, const std::fs::path &publicKeyPath, const std::string &passphrase) { + SSHClient::SSHClient(const std::string &host, int port, const std::string &user, const std::fs::path &publicKeyPath, const std::string &passphrase) { connect(host, port); authenticatePublicKey(user, publicKeyPath, passphrase); @@ -37,7 +37,7 @@ namespace hex::plugin::remote { throw std::runtime_error("Failed to initialize SFTP session"); } - SFTPClient::~SFTPClient() { + SSHClient::~SSHClient() { if (m_sftp) libssh2_sftp_shutdown(m_sftp); if (m_session) { libssh2_session_disconnect(m_session, "Normal Shutdown"); @@ -50,7 +50,7 @@ namespace hex::plugin::remote { #endif } - SFTPClient::SFTPClient(SFTPClient &&other) noexcept { + SSHClient::SSHClient(SSHClient &&other) noexcept { m_sftp = other.m_sftp; other.m_sftp = nullptr; @@ -65,7 +65,7 @@ namespace hex::plugin::remote { #endif } - SFTPClient& SFTPClient::operator=(SFTPClient &&other) noexcept { + SSHClient& SSHClient::operator=(SSHClient &&other) noexcept { if (this != &other) { if (m_sftp) libssh2_sftp_shutdown(m_sftp); if (m_session) { @@ -96,7 +96,7 @@ namespace hex::plugin::remote { } - void SFTPClient::connect(const std::string &host, int port) { + void SSHClient::connect(const std::string &host, int port) { addrinfo hints = {}, *res; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; @@ -136,19 +136,19 @@ namespace hex::plugin::remote { throw std::runtime_error("SSH handshake failed: " + getErrorString(m_session)); } - void SFTPClient::authenticatePassword(const std::string &user, const std::string &password) { + void SSHClient::authenticatePassword(const std::string &user, const std::string &password) { if (libssh2_userauth_password(m_session, user.c_str(), password.c_str())) throw std::runtime_error("Authentication failed: " + getErrorString(m_session)); } - void SFTPClient::authenticatePublicKey(const std::string &user, const std::fs::path &privateKeyPath, const std::string &) { + void SSHClient::authenticatePublicKey(const std::string &user, const std::fs::path &privateKeyPath, const std::string &) { auto result = libssh2_userauth_publickey_fromfile(m_session, user.c_str(), nullptr, wolv::util::toUTF8String(privateKeyPath).c_str(), nullptr); if (result) throw std::runtime_error("Authentication failed: " + getErrorString(m_session)); } - const std::vector& SFTPClient::listDirectory(const std::fs::path &path) { + const std::vector& SSHClient::listDirectory(const std::fs::path &path) { if (m_sftp == nullptr) return m_cachedFsItems; @@ -189,7 +189,7 @@ namespace hex::plugin::remote { return m_cachedFsItems; } - SFTPClient::RemoteFile SFTPClient::openFile(const std::fs::path &remotePath, OpenMode mode) { + std::unique_ptr SSHClient::openFileSFTP(const std::fs::path &remotePath, OpenMode mode) { int flags = 0; switch (mode) { @@ -209,7 +209,7 @@ namespace hex::plugin::remote { if (!handle) { long sftpError = libssh2_sftp_last_error(m_sftp); if (mode != OpenMode::Read && sftpError == LIBSSH2_FX_PERMISSION_DENIED) { - return openFile(remotePath, OpenMode::Read); + return openFileSFTP(remotePath, OpenMode::Read); } else { throw std::runtime_error("Failed to open remote file '" + pathString + "' - " + getErrorString(m_session) + @@ -217,10 +217,15 @@ namespace hex::plugin::remote { } } - return RemoteFile(handle, mode); + return std::make_unique(handle, mode); } - void SFTPClient::disconnect() { + std::unique_ptr SSHClient::openFileSSH(const std::fs::path &remotePath, OpenMode mode) { + auto pathString = wolv::util::toUTF8String(remotePath); + return std::make_unique(m_session, pathString, mode); + } + + void SSHClient::disconnect() { if (m_sftp != nullptr) { libssh2_sftp_shutdown(m_sftp); m_sftp = nullptr; @@ -234,7 +239,7 @@ namespace hex::plugin::remote { } - std::string SFTPClient::getErrorString(LIBSSH2_SESSION* session) const { + std::string SSHClient::getErrorString(LIBSSH2_SESSION* session) const { char *errorString; int length = 0; libssh2_session_last_error(session, &errorString, &length, false); @@ -242,31 +247,16 @@ namespace hex::plugin::remote { return fmt::format("{} ({})", std::string(errorString, static_cast(length)), libssh2_session_last_errno(session)); } - SFTPClient::RemoteFile::RemoteFile(LIBSSH2_SFTP_HANDLE* handle, OpenMode mode) : m_handle(handle), m_mode(mode) {} + RemoteFileSFTP::RemoteFileSFTP(LIBSSH2_SFTP_HANDLE* handle, SSHClient::OpenMode mode) : m_handle(handle), m_mode(mode) {} - SFTPClient::RemoteFile::~RemoteFile() { + RemoteFileSFTP::~RemoteFileSFTP() { if (m_handle) { libssh2_sftp_close(m_handle); m_handle = nullptr; } } - SFTPClient::RemoteFile::RemoteFile(RemoteFile &&other) noexcept - : m_handle(other.m_handle), m_atEOF(other.m_atEOF), m_mode(other.m_mode) { - other.m_handle = nullptr; - } - SFTPClient::RemoteFile& SFTPClient::RemoteFile::operator=(RemoteFile &&other) noexcept { - if (this != &other) { - if (m_handle) libssh2_sftp_close(m_handle); - m_handle = other.m_handle; - m_atEOF = other.m_atEOF; - m_mode = other.m_mode; - other.m_handle = nullptr; - } - return *this; - } - - size_t SFTPClient::RemoteFile::read(std::span buffer) { + size_t RemoteFileSFTP::read(std::span buffer) { auto dataSize = this->size(); auto offset = this->tell(); @@ -282,7 +272,7 @@ namespace hex::plugin::remote { return static_cast(n); } - size_t SFTPClient::RemoteFile::write(std::span buffer) { + size_t RemoteFileSFTP::write(std::span buffer) { if (buffer.empty()) return 0; @@ -293,16 +283,16 @@ namespace hex::plugin::remote { return static_cast(n); } - void SFTPClient::RemoteFile::seek(uint64_t offset) { + void RemoteFileSFTP::seek(uint64_t offset) { libssh2_sftp_seek64(m_handle, offset); m_atEOF = false; } - uint64_t SFTPClient::RemoteFile::tell() const { + u64 RemoteFileSFTP::tell() const { return libssh2_sftp_tell64(m_handle); } - u64 SFTPClient::RemoteFile::size() const { + u64 RemoteFileSFTP::size() const { LIBSSH2_SFTP_ATTRIBUTES attrs = {}; if (libssh2_sftp_fstat(m_handle, &attrs) != 0) return 0; @@ -310,18 +300,130 @@ namespace hex::plugin::remote { return attrs.filesize; } - bool SFTPClient::RemoteFile::eof() const { + bool RemoteFileSFTP::eof() const { return m_atEOF; } - void SFTPClient::RemoteFile::flush() { + void RemoteFileSFTP::flush() { libssh2_sftp_fsync(m_handle); } - void SFTPClient::RemoteFile::close() { + void RemoteFileSFTP::close() { libssh2_sftp_close(m_handle); m_handle = nullptr; } -} \ No newline at end of file + RemoteFileSSH::RemoteFileSSH(LIBSSH2_SESSION *handle, std::string path, SSHClient::OpenMode mode) + : RemoteFile(mode), m_handle(handle) { + m_readCommand = fmt::format("dd if=\"{0}\" skip={{0}} count={{1}} bs=1", path); + m_writeCommand = fmt::format("dd of=\"{0}\" seek={{0}} count={{1}} bs=1 conv=notrunc", path); + m_sizeCommand = "stat -c%s {0}"; + } + + RemoteFileSSH::~RemoteFileSSH() { + if (m_handle) { + m_handle = nullptr; + } + } + + size_t RemoteFileSSH::read(std::span buffer) { + auto offset = this->tell(); + + if (buffer.empty()) + return 0; + + auto result = executeCommand(fmt::format(fmt::runtime(m_readCommand), offset, buffer.size())); + + auto size = std::min(result.size(), buffer.size()); + std::memcpy(buffer.data(), result.data(), size); + + return size; + } + + size_t RemoteFileSSH::write(std::span buffer) { + auto offset = this->tell(); + + if (buffer.empty()) + return 0; + + // Send data via STDIN to dd command remotely + auto bytesWritten = executeCommand( + fmt::format(fmt::runtime(m_writeCommand), offset, buffer.size()), + buffer + ); + + return buffer.size(); + } + + void RemoteFileSSH::seek(uint64_t offset) { + m_seekPosition = offset; + } + + u64 RemoteFileSSH::tell() const { + return m_seekPosition; + } + + u64 RemoteFileSSH::size() const { + auto bytes = executeCommand(m_sizeCommand); + + u64 size = 0; + if (std::from_chars(reinterpret_cast(bytes.data()), reinterpret_cast(bytes.data() + bytes.size()), size).ec != std::errc()) { + return 0; + } + + return size; + } + + bool RemoteFileSSH::eof() const { + return m_atEOF; + } + + void RemoteFileSSH::flush() { + // Nothing to do + } + + void RemoteFileSSH::close() { + m_handle = nullptr; + } + + std::vector RemoteFileSSH::executeCommand(const std::string &command, std::span writeData) const { + std::lock_guard lock(m_mutex); + + LIBSSH2_CHANNEL* channel = libssh2_channel_open_session(m_handle); + if (!channel) { + return {}; + } + + ON_SCOPE_EXIT { + libssh2_channel_close(channel); + libssh2_channel_free(channel); + }; + + if (libssh2_channel_exec(channel, command.c_str()) != 0) { + return {}; + } + + if (!writeData.empty()) { + libssh2_channel_write(channel, reinterpret_cast(writeData.data()), writeData.size()); + } + + std::vector result; + std::array buffer; + while (true) { + const auto rc = libssh2_channel_read(channel, buffer.data(), buffer.size()); + if (rc > 0) { + result.insert(result.end(), buffer.data(), buffer.data() + rc); + } else if (rc == LIBSSH2_ERROR_EAGAIN) { + continue; + } else { + break; + } + } + + libssh2_channel_send_eof(channel); + + return result; + } + +} diff --git a/plugins/remote/source/content/providers/ssh_provider.cpp b/plugins/remote/source/content/providers/ssh_provider.cpp index 7a5d30667..6101e12a8 100644 --- a/plugins/remote/source/content/providers/ssh_provider.cpp +++ b/plugins/remote/source/content/providers/ssh_provider.cpp @@ -15,10 +15,10 @@ namespace hex::plugin::remote { if (!m_sftpClient.isConnected()) { try { if (m_authMethod == AuthMethod::Password) { - SFTPClient client(m_host, m_port, m_username, m_password); + SSHClient client(m_host, m_port, m_username, m_password); m_sftpClient = std::move(client); } else if (m_authMethod == AuthMethod::KeyFile) { - SFTPClient client(m_host, m_port, m_username, m_privateKeyPath, m_keyPassphrase); + SSHClient client(m_host, m_port, m_username, m_privateKeyPath, m_keyPassphrase); m_sftpClient = std::move(client); } } catch (const std::exception& e) { @@ -28,39 +28,46 @@ namespace hex::plugin::remote { } try { - m_remoteFile = m_sftpClient.openFile(m_remoteFilePath, SFTPClient::OpenMode::ReadWrite); + if (m_accessFileOverSSH) + m_remoteFile = m_sftpClient.openFileSSH(m_remoteFilePath, SSHClient::OpenMode::ReadWrite); + else + m_remoteFile = m_sftpClient.openFileSFTP(m_remoteFilePath, SSHClient::OpenMode::ReadWrite); } catch (const std::exception& e) { setErrorMessage(e.what()); return false; } - return m_remoteFile.isOpen(); + return m_remoteFile->isOpen(); } void SSHProvider::close() { - m_remoteFile.close(); + m_remoteFile->close(); m_sftpClient.disconnect(); m_remoteFilePath.clear(); } void SSHProvider::save() { - if (m_sftpClient.isConnected() && m_remoteFile.isOpen()) { - m_remoteFile.flush(); + if (m_sftpClient.isConnected() && m_remoteFile->isOpen()) { + m_remoteFile->flush(); } } void SSHProvider::readFromSource(u64 offset, void* buffer, size_t size) { - m_remoteFile.seek(offset); - std::ignore = m_remoteFile.read({ static_cast(buffer), size }); + m_remoteFile->seek(offset); + std::ignore = m_remoteFile->read({ static_cast(buffer), size }); } void SSHProvider::writeToSource(u64 offset, const void* buffer, size_t size) { - m_remoteFile.seek(offset); - std::ignore = m_remoteFile.write({ static_cast(buffer), size }); + m_remoteFile->seek(offset); + std::ignore = m_remoteFile->write({ static_cast(buffer), size }); } u64 SSHProvider::getSourceSize() const { - return m_remoteFile.size(); + auto size = m_remoteFile->size(); + if (size == 0) + return std::numeric_limits::max(); + else + return size; } std::string SSHProvider::getName() const { @@ -97,10 +104,10 @@ namespace hex::plugin::remote { if (ImGui::Button("hex.plugin.remote.ssh_provider.connect"_lang, ImVec2(ImGui::GetContentRegionAvail().x, 0))) { try { if (m_authMethod == AuthMethod::Password) { - SFTPClient client(m_host, m_port, m_username, m_password); + SSHClient client(m_host, m_port, m_username, m_password); m_sftpClient = std::move(client); } else if (m_authMethod == AuthMethod::KeyFile) { - SFTPClient client(m_host, m_port, m_username, m_privateKeyPath, m_keyPassphrase); + SSHClient client(m_host, m_port, m_username, m_privateKeyPath, m_keyPassphrase); m_sftpClient = std::move(client); } } catch (const std::exception& e) { @@ -144,6 +151,9 @@ namespace hex::plugin::remote { } ImGui::EndTable(); } + + ImGui::NewLine(); + ImGui::Checkbox("hex.plugin.remote.ssh_provider.ssh_access"_lang, &m_accessFileOverSSH); } return m_selectedFile; @@ -162,6 +172,7 @@ namespace hex::plugin::remote { } settings["remoteFilePath"] = wolv::util::toUTF8String(m_remoteFilePath); + settings["accessFileOverSSH"] = m_accessFileOverSSH; return Provider::storeSettings(settings); } @@ -181,6 +192,7 @@ namespace hex::plugin::remote { } m_remoteFilePath = settings.value("remoteFilePath", ""); + m_accessFileOverSSH = settings.value("accessFileOverSSH", false); } } diff --git a/plugins/remote/source/plugin_remote.cpp b/plugins/remote/source/plugin_remote.cpp index 8ae92b8dd..6e12d931a 100644 --- a/plugins/remote/source/plugin_remote.cpp +++ b/plugins/remote/source/plugin_remote.cpp @@ -17,9 +17,9 @@ IMHEX_PLUGIN_SETUP("Remote", "WerWolv", "Reading data from remote servers") { return romfs::get(path).string(); }); - hex::plugin::remote::SFTPClient::init(); + hex::plugin::remote::SSHClient::init(); AT_FINAL_CLEANUP { - hex::plugin::remote::SFTPClient::exit(); + hex::plugin::remote::SSHClient::exit(); }; hex::ContentRegistry::Provider::add();