feat: Added remote SSH file provider

This commit is contained in:
WerWolv 2025-07-13 11:24:43 +02:00
parent a89fb542b0
commit bdc108d021
22 changed files with 992 additions and 26 deletions

View File

@ -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"
}
}
],

View File

@ -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)

View File

@ -28,6 +28,7 @@ RDEPEND="${DEPEND}
app-arch/lzma
app-arch/zstd
app-arch/lz4
net-libs/libssh2
"
BDEPEND="${DEPEND}
dev-cpp/nlohmann_json

View File

@ -20,4 +20,5 @@ pacman -S $@ --needed \
bzip2 \
xz \
zstd \
lz4
lz4 \
libssh2

View File

@ -28,4 +28,5 @@ apt install -y \
libbz2-dev \
liblzma-dev \
libzstd-dev \
liblz4-dev
liblz4-dev \
libssh2-1-dev

View File

@ -18,4 +18,5 @@ dnf install -y \
zlib-devel \
bzip2-devel \
xz-devel \
lz4-devel
lz4-devel \
libssh2-devel

View File

@ -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

View File

@ -18,4 +18,5 @@ zypper install \
zlib-devel \
bzip3-devel \
xz-devel \
lz4-dev
lz4-dev \
libssh2-devel

3
dist/vcpkg.json vendored
View File

@ -11,6 +11,7 @@
"liblzma",
"zstd",
"glfw3",
"curl"
"curl",
"libssh2"
]
}

2
dist/web/Dockerfile vendored
View File

@ -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++ \

@ -1 +1 @@
Subproject commit a8f68e7222e94a5c202842d4bcfed4a600855eda
Subproject commit b7e3530aea19fc82c45d3c6d6beed0d459aca0ee

View File

@ -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

View File

@ -0,0 +1,59 @@
#pragma once
#include <hex/providers/provider.hpp>
#include <unordered_map>
#include <vector>
#include <mutex>
#include <shared_mutex>
#include <cstddef>
#include <cstdint>
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<uint8_t> data;
bool dirty = false;
};
size_t m_cacheBlockSize;
size_t m_maxBlocks;
mutable std::shared_mutex m_cacheMutex;
std::vector<std::optional<Block>> 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();
};
}

View File

@ -0,0 +1,130 @@
#include "hex/providers/cached_provider.hpp"
#include <algorithm>
#include <optional>
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<u8 *>(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<uint8_t> 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<const u8 *>(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<uint8_t> 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());
}
}

View File

@ -639,7 +639,7 @@ namespace hex::plugin::builtin {
if (provider->isDirty())
postfix += " (*)";
if (!provider->isWritable() && provider->getActualSize() != 0)
if (!provider->isWritable())
postfix += " (Read Only)";
}
}

View File

@ -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}
)

View File

@ -0,0 +1,130 @@
#pragma once
#include <string>
#include <vector>
#include <span>
#if defined(OS_WINDOWS)
#include <winsock2.h>
using SocketType = SOCKET;
#else
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
using SocketType = int;
#endif
#include <libssh2.h>
#include <libssh2_sftp.h>
#include <hex/helpers/fs.hpp>
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<FsItem>& 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<u8> buffer);
[[nodiscard]] size_t write(std::span<const u8> 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<FsItem> m_cachedFsItems;
};
}

View File

@ -0,0 +1,54 @@
#pragma once
#include <content/helpers/sftp_client.hpp>
#include <hex/providers/cached_provider.hpp>
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 };
};
}

View File

@ -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"
}
}

View File

@ -0,0 +1,323 @@
#include <content/helpers/sftp_client.hpp>
#include <hex/helpers/fmt.hpp>
#include <hex/helpers/logger.hpp>
#include <wolv/utils/string.hpp>
#if defined(OS_WINDOWS)
#include <ws2tcpip.h>
#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::FsItem>& 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<char, 512> 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<size_t>(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<u8> 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<char*>(buffer.data()), std::min(buffer.size_bytes(), size - offset));
if (n < 0)
return 0;
if (n == 0)
m_atEOF = true;
return static_cast<size_t>(n);
}
size_t SFTPClient::RemoteFile::write(std::span<const u8> buffer) {
if (buffer.empty())
return 0;
ssize_t n = libssh2_sftp_write(m_handle, reinterpret_cast<const char*>(buffer.data()), buffer.size_bytes());
if (n < 0)
return 0;
return static_cast<size_t>(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;
}
}

View File

@ -0,0 +1,185 @@
#include <content/providers/ssh_provider.hpp>
#include <imgui.h>
#include <fonts/vscode_icons.hpp>
#include <hex/ui/imgui_imhex_extensions.h>
#include <hex/helpers/utils.hpp>
#include <nlohmann/json.hpp>
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<u8*>(buffer), size });
}
void SSHProvider::writeToSource(u64 offset, const void* buffer, size_t size) {
m_remoteFile.seek(offset);
std::ignore = m_remoteFile.write({ static_cast<const u8*>(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", "");
}
}

View File

@ -0,0 +1,22 @@
#include <hex/plugin.hpp>
#include <hex/api/content_registry.hpp>
#include <hex/helpers/logger.hpp>
#include <pl/api.hpp>
#include <romfs/romfs.hpp>
#include <libssh2.h>
#include <content/helpers/sftp_client.hpp>
#include <content/providers/ssh_provider.hpp>
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<hex::plugin::remote::SSHProvider>();
}