diff --git a/CMakePresets.json b/CMakePresets.json index d77fa470b..2681eae26 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -52,7 +52,8 @@ "generator": "Ninja", "binaryDir": "${sourceDir}/build/${presetName}", "cacheVariables": { - "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "VCPKG_MANIFEST_DIR": "${sourceDir}/dist" } } ], diff --git a/cmake/modules/Findlibssh2.cmake b/cmake/modules/Findlibssh2.cmake new file mode 100644 index 000000000..65ae8dabc --- /dev/null +++ b/cmake/modules/Findlibssh2.cmake @@ -0,0 +1,15 @@ +find_path(LIBSSH2_INCLUDE_DIR libssh2.h) + +find_library(LIBSSH2_LIBRARY NAMES ssh2 libssh2) + +if(LIBSSH2_INCLUDE_DIR) + file(STRINGS "${LIBSSH2_INCLUDE_DIR}/libssh2.h" libssh2_version_str REGEX "^#define[\t ]+LIBSSH2_VERSION[\t ]+\"(.*)\"") + string(REGEX REPLACE "^.*\"([^\"]+)\"" "\\1" LIBSSH2_VERSION "${libssh2_version_str}") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(LibSSH2 + REQUIRED_VARS LIBSSH2_LIBRARY LIBSSH2_INCLUDE_DIR + VERSION_VAR LIBSSH2_VERSION) + +mark_as_advanced(LIBSSH2_INCLUDE_DIR LIBSSH2_LIBRARY) diff --git a/dist/ImHex-9999.ebuild b/dist/ImHex-9999.ebuild index 3fc418d60..ffc0c6af6 100644 --- a/dist/ImHex-9999.ebuild +++ b/dist/ImHex-9999.ebuild @@ -28,6 +28,7 @@ RDEPEND="${DEPEND} app-arch/lzma app-arch/zstd app-arch/lz4 + net-libs/libssh2 " BDEPEND="${DEPEND} dev-cpp/nlohmann_json diff --git a/dist/get_deps_archlinux.sh b/dist/get_deps_archlinux.sh index 8d5f3f4a6..a41611e97 100755 --- a/dist/get_deps_archlinux.sh +++ b/dist/get_deps_archlinux.sh @@ -20,4 +20,5 @@ pacman -S $@ --needed \ bzip2 \ xz \ zstd \ - lz4 + lz4 \ + libssh2 diff --git a/dist/get_deps_debian.sh b/dist/get_deps_debian.sh index 169f93a2e..64f6e9992 100755 --- a/dist/get_deps_debian.sh +++ b/dist/get_deps_debian.sh @@ -28,4 +28,5 @@ apt install -y \ libbz2-dev \ liblzma-dev \ libzstd-dev \ - liblz4-dev + liblz4-dev \ + libssh2-1-dev diff --git a/dist/get_deps_fedora.sh b/dist/get_deps_fedora.sh index c5f006e7b..f2caa4b08 100755 --- a/dist/get_deps_fedora.sh +++ b/dist/get_deps_fedora.sh @@ -18,4 +18,5 @@ dnf install -y \ zlib-devel \ bzip2-devel \ xz-devel \ - lz4-devel \ No newline at end of file + lz4-devel \ + libssh2-devel \ No newline at end of file diff --git a/dist/get_deps_msys2.sh b/dist/get_deps_msys2.sh index 83035abcf..9bc9ee41d 100755 --- a/dist/get_deps_msys2.sh +++ b/dist/get_deps_msys2.sh @@ -2,20 +2,21 @@ pacman -S --needed --noconfirm pactoys unzip pacboy -S --needed --noconfirm \ - gcc:p \ - lld:p \ - cmake:p \ - ccache:p \ - glfw:p \ - file:p \ - curl-winssl:p \ - mbedtls:p \ - freetype:p \ - dlfcn:p \ - ninja:p \ - capstone:p \ - zlib:p \ - bzip2:p \ - xz:p \ - zstd:p \ - lz4:p + gcc:p \ + lld:p \ + cmake:p \ + ccache:p \ + glfw:p \ + file:p \ + curl-winssl:p \ + mbedtls:p \ + freetype:p \ + dlfcn:p \ + ninja:p \ + capstone:p \ + zlib:p \ + bzip2:p \ + xz:p \ + zstd:p \ + lz4:p \ + libssh2-wincng:p diff --git a/dist/get_deps_tumbleweed.sh b/dist/get_deps_tumbleweed.sh index f625d2135..a3ef5c4bf 100755 --- a/dist/get_deps_tumbleweed.sh +++ b/dist/get_deps_tumbleweed.sh @@ -18,4 +18,5 @@ zypper install \ zlib-devel \ bzip3-devel \ xz-devel \ - lz4-dev + lz4-dev \ + libssh2-devel diff --git a/dist/vcpkg.json b/dist/vcpkg.json index 71ed2f939..ad3f4be61 100644 --- a/dist/vcpkg.json +++ b/dist/vcpkg.json @@ -11,6 +11,7 @@ "liblzma", "zstd", "glfw3", - "curl" + "curl", + "libssh2" ] } \ No newline at end of file diff --git a/dist/web/Dockerfile b/dist/web/Dockerfile index a01f021e3..46f4824a0 100644 --- a/dist/web/Dockerfile +++ b/dist/web/Dockerfile @@ -60,7 +60,7 @@ cmake /imhex -G "Ninja" \ -DIMHEX_OFFLINE_BUILD=ON \ -DIMHEX_STATIC_LINK_PLUGINS=ON \ - -DIMHEX_EXCLUDE_PLUGINS="script_loader" \ + -DIMHEX_EXCLUDE_PLUGINS="script_loader;remote" \ -DIMHEX_COMPRESS_DEBUG_INFO=OFF \ -DNATIVE_CMAKE_C_COMPILER=gcc \ -DNATIVE_CMAKE_CXX_COMPILER=g++ \ diff --git a/lib/external/libwolv b/lib/external/libwolv index a8f68e722..b7e3530ae 160000 --- a/lib/external/libwolv +++ b/lib/external/libwolv @@ -1 +1 @@ -Subproject commit a8f68e7222e94a5c202842d4bcfed4a600855eda +Subproject commit b7e3530aea19fc82c45d3c6d6beed0d459aca0ee diff --git a/lib/libimhex/CMakeLists.txt b/lib/libimhex/CMakeLists.txt index 4584ddb9e..49ab76023 100644 --- a/lib/libimhex/CMakeLists.txt +++ b/lib/libimhex/CMakeLists.txt @@ -45,6 +45,7 @@ set(LIBIMHEX_SOURCES source/test/tests.cpp source/providers/provider.cpp + source/providers/cached_provider.cpp source/providers/memory_provider.cpp source/providers/undo/stack.cpp diff --git a/lib/libimhex/include/hex/providers/cached_provider.hpp b/lib/libimhex/include/hex/providers/cached_provider.hpp new file mode 100644 index 000000000..f92abfc85 --- /dev/null +++ b/lib/libimhex/include/hex/providers/cached_provider.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace hex::prv { + + /** + * @brief A base class for providers that want to cache data in memory. + * Thread-safe for concurrent reads/writes. Reads are cached in memory. + * Subclasses must implement readFromSource and writeToSource. + */ + class CachedProvider : public Provider { + public: + CachedProvider(size_t cacheBlockSize = 4096, size_t maxBlocks = 1024); + ~CachedProvider() override; + + bool open() override; + void close() override; + + void readRaw(u64 offset, void *buffer, size_t size) override; + void writeRaw(u64 offset, const void *buffer, size_t size) override; + void resizeRaw(u64 newSize) override; + + u64 getActualSize() const override; + + protected: + virtual void readFromSource(uint64_t offset, void* buffer, size_t size) = 0; + virtual void writeToSource(uint64_t offset, const void* buffer, size_t size) = 0; + virtual void resizeSource(uint64_t newSize) { std::ignore = newSize; } + virtual u64 getSourceSize() const = 0; + + void clearCache(); + + struct Block { + uint64_t index; + std::vector data; + bool dirty = false; + }; + + size_t m_cacheBlockSize; + size_t m_maxBlocks; + mutable std::shared_mutex m_cacheMutex; + std::vector> m_cache; + mutable u64 m_cachedSize = 0; + + constexpr u64 calcBlockIndex(u64 offset) const { return offset / m_cacheBlockSize; } + constexpr size_t calcBlockOffset(u64 offset) const { return offset % m_cacheBlockSize; } + + void evictIfNeeded(); + }; + +} diff --git a/lib/libimhex/source/providers/cached_provider.cpp b/lib/libimhex/source/providers/cached_provider.cpp new file mode 100644 index 000000000..ea118d09d --- /dev/null +++ b/lib/libimhex/source/providers/cached_provider.cpp @@ -0,0 +1,130 @@ +#include "hex/providers/cached_provider.hpp" +#include +#include + +namespace hex::prv { + + CachedProvider::CachedProvider(size_t cacheBlockSize, size_t maxBlocks) + : m_cacheBlockSize(cacheBlockSize), m_maxBlocks(maxBlocks), m_cache(maxBlocks) {} + + CachedProvider::~CachedProvider() { + clearCache(); + } + + bool CachedProvider::open() { + clearCache(); + return true; + } + + void CachedProvider::close() { + clearCache(); + } + + void CachedProvider::readRaw(u64 offset, void* buffer, size_t size) { + if (!isAvailable() || !isReadable()) + return; + + auto out = static_cast(buffer); + while (size > 0) { + const auto blockIndex = calcBlockIndex(offset); + const auto blockOffset = calcBlockOffset(offset); + const auto toRead = std::min(m_cacheBlockSize - blockOffset, size); + const auto cacheSlot = blockIndex % m_maxBlocks; + + { + std::shared_lock lock(m_cacheMutex); + const auto &slot = m_cache[cacheSlot]; + if (slot && slot->index == blockIndex) { + std::copy_n(slot->data.begin() + blockOffset, toRead, out); + + out += toRead; + offset += toRead; + size -= toRead; + continue; + } + } + + std::vector blockData(m_cacheBlockSize); + readFromSource(blockIndex * m_cacheBlockSize, blockData.data(), m_cacheBlockSize); + + { + std::unique_lock lock(m_cacheMutex); + m_cache[cacheSlot] = Block{blockIndex, std::move(blockData), false}; + std::copy_n(m_cache[cacheSlot]->data.begin() + blockOffset, toRead, out); + } + + out += toRead; + offset += toRead; + size -= toRead; + } + } + + void CachedProvider::writeRaw(u64 offset, const void* buffer, size_t size) { + if (!isAvailable() || !isWritable()) + return; + + auto in = static_cast(buffer); + while (size > 0) { + const auto blockIndex = calcBlockIndex(offset); + const auto blockOffset = calcBlockOffset(offset); + const auto toWrite = std::min(m_cacheBlockSize - blockOffset, size); + const auto cacheSlot = blockIndex % m_maxBlocks; + + { + std::unique_lock lock(m_cacheMutex); + auto& slot = m_cache[cacheSlot]; + if (!slot || slot->index != blockIndex) { + std::vector blockData(m_cacheBlockSize); + readFromSource(blockIndex * m_cacheBlockSize, blockData.data(), m_cacheBlockSize); + slot = Block { blockIndex, std::move(blockData), false }; + } + + std::copy_n(in, toWrite, slot->data.begin() + blockOffset); + slot->dirty = true; + } + + writeToSource(offset, in, toWrite); + + in += toWrite; + offset += toWrite; + size -= toWrite; + } + } + + void CachedProvider::resizeRaw(u64 newSize) { + clearCache(); + + resizeSource(newSize); + } + + + u64 CachedProvider::getActualSize() const { + if (!isAvailable()) + return 0; + + if (m_cachedSize == 0) { + std::unique_lock lock(m_cacheMutex); + m_cachedSize = getSourceSize(); + } + + return m_cachedSize; + } + + + void CachedProvider::clearCache() { + std::unique_lock lock(m_cacheMutex); + + for (auto& slot : m_cache) + slot.reset(); + + m_cachedSize = 0; + } + + void CachedProvider::evictIfNeeded() { + if (m_cache.size() < m_maxBlocks) + return; + + m_cache.erase(m_cache.begin()); + } + +} diff --git a/plugins/builtin/source/content/window_decoration.cpp b/plugins/builtin/source/content/window_decoration.cpp index d9c95b5b8..00b197c6c 100644 --- a/plugins/builtin/source/content/window_decoration.cpp +++ b/plugins/builtin/source/content/window_decoration.cpp @@ -639,7 +639,7 @@ namespace hex::plugin::builtin { if (provider->isDirty()) postfix += " (*)"; - if (!provider->isWritable() && provider->getActualSize() != 0) + if (!provider->isWritable()) postfix += " (Read Only)"; } } diff --git a/plugins/remote/CMakeLists.txt b/plugins/remote/CMakeLists.txt new file mode 100644 index 000000000..ceb1d739b --- /dev/null +++ b/plugins/remote/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.16) + +include(ImHexPlugin) + +find_package(libssh2 REQUIRED) + +add_imhex_plugin( + NAME + remote + SOURCES + source/plugin_remote.cpp + source/content/helpers/sftp_client.cpp + source/content/providers/ssh_provider.cpp + + INCLUDES + include + ${LIBSSH2_INCLUDE_DIR} + + LIBRARIES + ui + fonts + ${LIBSSH2_LIBRARY} +) diff --git a/plugins/remote/include/content/helpers/sftp_client.hpp b/plugins/remote/include/content/helpers/sftp_client.hpp new file mode 100644 index 000000000..edc477006 --- /dev/null +++ b/plugins/remote/include/content/helpers/sftp_client.hpp @@ -0,0 +1,130 @@ +#pragma once + +#include +#include +#include + +#if defined(OS_WINDOWS) + #include + using SocketType = SOCKET; +#else + #include + #include + #include + #include + using SocketType = int; +#endif + +#include +#include + +#include + +namespace hex::plugin::remote { + + class SFTPClient { + public: + SFTPClient() = default; + SFTPClient(const std::string &host, + int port, + const std::string &user, + const std::string &password); + + SFTPClient(const std::string &host, + int port, + const std::string &user, + const std::string &privateKeyPath, + const std::string &passphrase); + + ~SFTPClient(); + + SFTPClient(const SFTPClient&) = delete; + SFTPClient& operator=(const SFTPClient&) = delete; + + SFTPClient(SFTPClient &&other) noexcept; + SFTPClient& operator=(SFTPClient &&other) noexcept; + + struct FsItem { + std::string name; + LIBSSH2_SFTP_ATTRIBUTES attributes; + + bool isDirectory() const { + return (attributes.flags & LIBSSH2_SFTP_ATTR_PERMISSIONS) && + (attributes.permissions & LIBSSH2_SFTP_S_IRWXU) == LIBSSH2_SFTP_S_IRWXU; + } + + bool isRegularFile() const { + return (attributes.flags & LIBSSH2_SFTP_ATTR_PERMISSIONS) && + (attributes.permissions & LIBSSH2_SFTP_S_IFREG) == LIBSSH2_SFTP_S_IFREG; + } + }; + + const std::vector& listDirectory(const std::fs::path& path); + + enum class OpenMode { Read, Write, ReadWrite }; + + class RemoteFile { + public: + RemoteFile() = default; + RemoteFile(LIBSSH2_SFTP_HANDLE* handle, OpenMode mode); + ~RemoteFile(); + + RemoteFile(const RemoteFile&) = delete; + RemoteFile& operator=(const RemoteFile&) = delete; + RemoteFile(RemoteFile &&other) noexcept; + RemoteFile& operator=(RemoteFile &&other) noexcept; + + [[nodiscard]] bool isOpen() const { + return m_handle != nullptr; + } + + [[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; + + bool eof() const; + void flush(); + void close(); + + [[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); + void disconnect(); + + [[nodiscard]] bool isConnected() const { + return m_sftp != nullptr; + } + + static void init(); + static void exit(); + + private: + void connect(const std::string &host, int port); + void authenticatePassword(const std::string &user, const std::string &password); + void authenticatePublicKey(const std::string &user, const std::string &privateKeyPath, const std::string &passphrase); + + std::string getErrorString(LIBSSH2_SESSION* session) const; + + private: + #if defined(OS_WINDOWS) + SocketType m_sock = 0; + #else + SocketType m_sock = -1; + #endif + + LIBSSH2_SESSION* m_session = nullptr; + LIBSSH2_SFTP* m_sftp = nullptr; + + std::fs::path m_cachedDirectoryPath; + std::vector m_cachedFsItems; + }; + +} diff --git a/plugins/remote/include/content/providers/ssh_provider.hpp b/plugins/remote/include/content/providers/ssh_provider.hpp new file mode 100644 index 000000000..29fd53115 --- /dev/null +++ b/plugins/remote/include/content/providers/ssh_provider.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +namespace hex::plugin::remote { + + class SSHProvider : public hex::prv::CachedProvider { + public: + bool isAvailable() const override { return m_remoteFile.isOpen(); } + bool isReadable() const override { return isAvailable(); } + bool isWritable() const override { return m_remoteFile.getOpenMode() != SFTPClient::OpenMode::Read; } + bool isResizable() const override { return false; } + bool isSavable() const override { return isWritable(); } + + bool open() override; + void close() override; + void save() override; + + void readFromSource(uint64_t offset, void* buffer, size_t size) override; + void writeToSource(uint64_t offset, const void* buffer, size_t size) override; + + u64 getSourceSize() const override; + UnlocalizedString getTypeName() const override { return "hex.plugin.remote.ssh_provider"; } + std::string getName() const override; + + bool drawLoadInterface() override; + bool hasLoadInterface() const override { return true; } + + void loadSettings(const nlohmann::json &settings) override; + nlohmann::json storeSettings(nlohmann::json settings) const override; + + enum class AuthMethod { + Password, + KeyFile + }; + + private: + SFTPClient m_sftpClient; + SFTPClient::RemoteFile m_remoteFile; + + std::string m_host; + int m_port = 22; + std::string m_username; + std::string m_password; + std::string m_privateKeyPath; + std::string m_keyPassphrase; + AuthMethod m_authMethod = AuthMethod::Password; + + bool m_selectedFile = false; + std::fs::path m_remoteFilePath = { "/", std::fs::path::format::generic_format }; + }; + +} \ No newline at end of file diff --git a/plugins/remote/romfs/lang/en_US.json b/plugins/remote/romfs/lang/en_US.json new file mode 100644 index 000000000..f80474600 --- /dev/null +++ b/plugins/remote/romfs/lang/en_US.json @@ -0,0 +1,16 @@ +{ + "code": "en-US", + "language": "English", + "country": "United States", + "fallback": true, + "translations": { + "hex.plugin.remote.ssh_provider": "Remote SSH File", + "hex.plugin.remote.ssh_provider.host": "Host", + "hex.plugin.remote.ssh_provider.port": "Port", + "hex.plugin.remote.ssh_provider.username": "Username", + "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" + } +} diff --git a/plugins/remote/source/content/helpers/sftp_client.cpp b/plugins/remote/source/content/helpers/sftp_client.cpp new file mode 100644 index 000000000..f61ed3095 --- /dev/null +++ b/plugins/remote/source/content/helpers/sftp_client.cpp @@ -0,0 +1,323 @@ +#include +#include +#include +#include + +#if defined(OS_WINDOWS) + #include +#endif + +namespace hex::plugin::remote { + + void SFTPClient::init() { + libssh2_init(0); + } + + void SFTPClient::exit() { + libssh2_exit(); + } + + SFTPClient::SFTPClient(const std::string &host, int port, const std::string &user, const std::string &password) { + connect(host, port); + authenticatePassword(user, password); + + m_sftp = libssh2_sftp_init(m_session); + if (!m_sftp) + throw std::runtime_error("Failed to initialize SFTP session"); + } + + SFTPClient::SFTPClient(const std::string &host, int port, const std::string &user, const std::string &publicKeyPath, const std::string &passphrase) { + connect(host, port); + authenticatePublicKey(user, publicKeyPath, passphrase); + + m_sftp = libssh2_sftp_init(m_session); + if (!m_sftp) + throw std::runtime_error("Failed to initialize SFTP session"); + } + + SFTPClient::~SFTPClient() { + if (m_sftp) libssh2_sftp_shutdown(m_sftp); + if (m_session) { + libssh2_session_disconnect(m_session, "Normal Shutdown"); + libssh2_session_free(m_session); + } + #if defined(OS_WINDOWS) + if (m_sock != INVALID_SOCKET) closesocket(m_sock); + #else + if (m_sock != -1) close(m_sock); + #endif + } + + SFTPClient::SFTPClient(SFTPClient &&other) noexcept { + m_sftp = other.m_sftp; + other.m_sftp = nullptr; + + m_session = other.m_session; + other.m_session = nullptr; + + m_sock = other.m_sock; + #if defined(OS_WINDOWS) + other.m_sock = INVALID_SOCKET; + #else + other.m_sock = -1; + #endif + } + + SFTPClient& SFTPClient::operator=(SFTPClient &&other) noexcept { + if (this != &other) { + if (m_sftp) libssh2_sftp_shutdown(m_sftp); + if (m_session) { + libssh2_session_disconnect(m_session, "Normal Shutdown"); + libssh2_session_free(m_session); + } + #if defined(OS_WINDOWS) + if (m_sock != INVALID_SOCKET) closesocket(m_sock); + #else + if (m_sock != -1) close(m_sock); + #endif + + m_sftp = other.m_sftp; + other.m_sftp = nullptr; + + m_session = other.m_session; + other.m_session = nullptr; + + m_sock = other.m_sock; + #if defined(OS_WINDOWS) + other.m_sock = INVALID_SOCKET; + #else + other.m_sock = -1; + #endif + } + + return *this; + } + + + void SFTPClient::connect(const std::string &host, int port) { + addrinfo hints = {}, *res; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + auto service = std::to_string(port); + if (getaddrinfo(host.c_str(), service.c_str(), &hints, &res) != 0) + throw std::runtime_error("getaddrinfo failed"); + + m_sock = ::socket(res->ai_family, res->ai_socktype, res->ai_protocol); + #if defined(OS_WINDOWS) + if (m_sock == INVALID_SOCKET) + #else + if (m_sock == -1) + #endif + throw std::runtime_error("Socket creation failed"); + + if (::connect(m_sock, res->ai_addr, res->ai_addrlen) != 0) { + #if defined(OS_WINDOWS) + closesocket(m_sock); + #else + close(m_sock); + #endif + + freeaddrinfo(res); + throw std::runtime_error("Connection to host failed"); + } + + freeaddrinfo(res); + + m_session = libssh2_session_init(); + if (!m_session) + throw std::runtime_error("SSH session init failed"); + + libssh2_session_set_blocking(m_session, true); + + if (libssh2_session_handshake(m_session, m_sock)) + throw std::runtime_error("SSH handshake failed: " + getErrorString(m_session)); + } + + void SFTPClient::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::string &privateKeyPath, const std::string &) { + auto result = libssh2_userauth_publickey_fromfile(m_session, user.c_str(), nullptr, 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) { + if (m_sftp == nullptr) + return m_cachedFsItems; + + if (path == m_cachedDirectoryPath) + return m_cachedFsItems; + + m_cachedFsItems.clear(); + m_cachedDirectoryPath = path; + + auto pathString = wolv::util::toUTF8String(path); + LIBSSH2_SFTP_HANDLE* dir = libssh2_sftp_opendir(m_sftp, pathString.c_str()); + if (!dir) + return m_cachedFsItems; + + std::array buffer; + LIBSSH2_SFTP_ATTRIBUTES attrs; + + while (libssh2_sftp_readdir(dir, buffer.data(), buffer.size(), &attrs) > 0) { + auto nameString = std::string_view(buffer.data()); + if (nameString == "." || nameString == "..") + continue; + + m_cachedFsItems.emplace_back(nameString.data(), attrs); + } + + libssh2_sftp_closedir(dir); + + // Sort the items by name, directories first + std::sort(m_cachedFsItems.begin(), m_cachedFsItems.end(), [](const FsItem &a, const FsItem &b) { + if (a.isDirectory() && !b.isDirectory()) + return true; + if (!a.isDirectory() && b.isDirectory()) + return false; + return a.name < b.name; + }); + + + return m_cachedFsItems; + } + + SFTPClient::RemoteFile SFTPClient::openFile(const std::fs::path &remotePath, OpenMode mode) { + int flags = 0; + + switch (mode) { + case OpenMode::Read: + flags = LIBSSH2_FXF_READ; + break; + case OpenMode::Write: + flags = LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT | LIBSSH2_FXF_TRUNC; + break; + case OpenMode::ReadWrite: + flags = LIBSSH2_FXF_READ | LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT; + break; + } + + auto pathString = wolv::util::toUTF8String(remotePath); + LIBSSH2_SFTP_HANDLE* handle = libssh2_sftp_open(m_sftp, pathString.c_str(), flags, 0); + if (!handle) { + long sftpError = libssh2_sftp_last_error(m_sftp); + if (mode != OpenMode::Read && sftpError == LIBSSH2_FX_PERMISSION_DENIED) { + return openFile(remotePath, OpenMode::Read); + } else { + throw std::runtime_error("Failed to open remote file '" + pathString + + "' - " + getErrorString(m_session) + + " (SFTP error: " + std::to_string(sftpError) + ")"); + } + } + + return RemoteFile(handle, mode); + } + + void SFTPClient::disconnect() { + if (m_sftp != nullptr) { + libssh2_sftp_shutdown(m_sftp); + m_sftp = nullptr; + } + + if (m_session != nullptr) { + libssh2_session_disconnect(m_session, "Disconnecting"); + libssh2_session_free(m_session); + m_session = nullptr; + } + } + + + std::string SFTPClient::getErrorString(LIBSSH2_SESSION* session) const { + char *errorString; + int length = 0; + libssh2_session_last_error(session, &errorString, &length, false); + + return hex::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) {} + + SFTPClient::RemoteFile::~RemoteFile() { + 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) { + 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; + other.m_handle = nullptr; + } + return *this; + } + + size_t SFTPClient::RemoteFile::read(std::span buffer) { + auto size = this->size(); + auto offset = this->tell(); + + if (offset > size || buffer.empty()) + return 0; + + ssize_t n = libssh2_sftp_read(m_handle, reinterpret_cast(buffer.data()), std::min(buffer.size_bytes(), size - offset)); + if (n < 0) + return 0; + if (n == 0) + m_atEOF = true; + + return static_cast(n); + } + + size_t SFTPClient::RemoteFile::write(std::span buffer) { + if (buffer.empty()) + return 0; + + ssize_t n = libssh2_sftp_write(m_handle, reinterpret_cast(buffer.data()), buffer.size_bytes()); + if (n < 0) + return 0; + + return static_cast(n); + } + + void SFTPClient::RemoteFile::seek(uint64_t offset) { + libssh2_sftp_seek64(m_handle, offset); + m_atEOF = false; + } + + uint64_t SFTPClient::RemoteFile::tell() const { + return libssh2_sftp_tell64(m_handle); + } + + u64 SFTPClient::RemoteFile::size() const { + LIBSSH2_SFTP_ATTRIBUTES attrs = {}; + if (libssh2_sftp_fstat(m_handle, &attrs) != 0) + return 0; + + return attrs.filesize; + } + + bool SFTPClient::RemoteFile::eof() const { + return m_atEOF; + } + + void SFTPClient::RemoteFile::flush() { + libssh2_sftp_fsync(m_handle); + } + + void SFTPClient::RemoteFile::close() { + libssh2_sftp_close(m_handle); + m_handle = nullptr; + } + + +} \ No newline at end of file diff --git a/plugins/remote/source/content/providers/ssh_provider.cpp b/plugins/remote/source/content/providers/ssh_provider.cpp new file mode 100644 index 000000000..6a6845442 --- /dev/null +++ b/plugins/remote/source/content/providers/ssh_provider.cpp @@ -0,0 +1,185 @@ +#include + +#include +#include +#include +#include + +#include + +namespace hex::plugin::remote { + + bool SSHProvider::open() { + if (!m_sftpClient.isConnected()) { + try { + if (m_authMethod == AuthMethod::Password) { + SFTPClient 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); + m_sftpClient = std::move(client); + } + } catch (const std::exception& e) { + return false; + } + } + + try { + m_remoteFile = m_sftpClient.openFile(m_remoteFilePath, SFTPClient::OpenMode::ReadWrite); + } catch (const std::exception& e) { + setErrorMessage(e.what()); + return false; + } + + return m_remoteFile.isOpen(); + } + + void SSHProvider::close() { + m_remoteFile.close(); + m_sftpClient.disconnect(); + m_remoteFilePath.clear(); + } + + void SSHProvider::save() { + 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 }); + } + + void SSHProvider::writeToSource(u64 offset, const void* buffer, size_t size) { + m_remoteFile.seek(offset); + std::ignore = m_remoteFile.write({ static_cast(buffer), size }); + } + + u64 SSHProvider::getSourceSize() const { + return m_remoteFile.size(); + } + + std::string SSHProvider::getName() const { + return hex::format("{} [{}@{}:{}]", m_remoteFilePath.filename().string(), m_username, m_host, m_port); + } + + + bool SSHProvider::drawLoadInterface() { + if (!m_sftpClient.isConnected()) { + ImGui::InputText("hex.plugin.remote.ssh_provider.host"_lang, m_host); + ImGui::InputInt("hex.plugin.remote.ssh_provider.port"_lang, &m_port, 0, 0); + ImGui::InputText("hex.plugin.remote.ssh_provider.username"_lang, m_username); + + ImGui::NewLine(); + + if (ImGui::BeginTabBar("##SSHProviderLoadInterface")) { + if (ImGui::BeginTabItem("hex.plugin.remote.ssh_provider.password"_lang)) { + m_authMethod = AuthMethod::Password; + ImGui::InputText("hex.plugin.remote.ssh_provider.password"_lang, m_password, ImGuiInputTextFlags_Password); + ImGui::NewLine(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("hex.plugin.remote.ssh_provider.key_file"_lang)) { + m_authMethod = AuthMethod::KeyFile; + ImGui::InputText("hex.plugin.remote.ssh_provider.key_file"_lang, m_privateKeyPath); + ImGui::InputText("hex.plugin.remote.ssh_provider.passphrase"_lang, m_keyPassphrase, ImGuiInputTextFlags_Password); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + + ImGui::NewLine(); + + 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); + m_sftpClient = std::move(client); + } else if (m_authMethod == AuthMethod::KeyFile) { + SFTPClient client(m_host, m_port, m_username, m_privateKeyPath, m_keyPassphrase); + m_sftpClient = std::move(client); + } + } catch (const std::exception& e) { + return false; + } + } + } else { + std::string pathString = wolv::util::toUTF8String(m_remoteFilePath); + if (ImGui::InputText("##RemoteFilePath", pathString)) { + m_remoteFilePath = pathString; + } + + if (ImGui::BeginTable("##RemoteFileList", 2, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY, ImVec2(0, 200_scaled))) { + ImGui::TableSetupColumn("##Icon", ImGuiTableColumnFlags_WidthFixed, 20_scaled); + ImGui::TableSetupColumn("##Name", ImGuiTableColumnFlags_WidthStretch); + + if (m_remoteFilePath.has_parent_path()) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(ICON_VS_FOLDER); + + ImGui::TableNextColumn(); + ImGui::Selectable("..", false, ImGuiSelectableFlags_NoAutoClosePopups); + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + m_remoteFilePath = m_remoteFilePath.parent_path(); + } + } + + for (const auto &entry : m_sftpClient.listDirectory(m_selectedFile ? m_remoteFilePath.parent_path() : m_remoteFilePath)) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(entry.isDirectory() ? ICON_VS_FOLDER : ICON_VS_FILE); + + ImGui::TableNextColumn(); + ImGui::Selectable(entry.name.c_str(), m_remoteFilePath.filename() == entry.name, ImGuiSelectableFlags_NoAutoClosePopups); + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + m_selectedFile = entry.isRegularFile(); + m_remoteFilePath /= entry.name; + } + } + ImGui::EndTable(); + } + } + + return m_selectedFile; + } + + nlohmann::json SSHProvider::storeSettings(nlohmann::json settings) const { + settings["host"] = m_host; + settings["port"] = m_port; + settings["username"] = m_username; + settings["authMethod"] = m_authMethod == AuthMethod::Password ? "password" : "key_file"; + if (m_authMethod == AuthMethod::Password) { + settings["password"] = m_password; + } else { + settings["privateKeyPath"] = m_privateKeyPath; + settings["keyPassphrase"] = m_keyPassphrase; + } + + settings["remoteFilePath"] = wolv::util::toUTF8String(m_remoteFilePath); + + return Provider::storeSettings(settings); + } + + void SSHProvider::loadSettings(const nlohmann::json &settings) { + Provider::loadSettings(settings); + + m_host = settings.value("host", ""); + m_port = settings.value("port", 22); + m_username = settings.value("username", ""); + m_authMethod = settings.value("authMethod", "password") == "password" ? AuthMethod::Password : AuthMethod::KeyFile; + if (m_authMethod == AuthMethod::Password) { + m_password = settings.value("password", ""); + } else { + m_privateKeyPath = settings.value("privateKeyPath", ""); + m_keyPassphrase = settings.value("keyPassphrase", ""); + } + + m_remoteFilePath = settings.value("remoteFilePath", ""); + } + + + + +} \ No newline at end of file diff --git a/plugins/remote/source/plugin_remote.cpp b/plugins/remote/source/plugin_remote.cpp new file mode 100644 index 000000000..0b54b1f8c --- /dev/null +++ b/plugins/remote/source/plugin_remote.cpp @@ -0,0 +1,22 @@ +#include + +#include +#include + +#include + +#include + +#include +#include +#include + +IMHEX_PLUGIN_SETUP("Remote", "WerWolv", "Reading data from remote servers") { + hex::log::debug("Using romfs: '{}'", romfs::name()); + for (auto &path : romfs::list("lang")) + hex::ContentRegistry::Language::addLocalization(nlohmann::json::parse(romfs::get(path).string())); + + hex::plugin::remote::SFTPClient::init(); + + hex::ContentRegistry::Provider::add(); +}