#include #include #include #include #include #include #include #include #include #include "jni.hpp" #include "libssh2.h" #include "libssh2_sftp.h" namespace { class SftpOwner { public: SftpOwner(std::shared_ptr ssh_session, LIBSSH2_SFTP* sftp) : ssh_session_(std::move(ssh_session)), sftp_(sftp) {} ~SftpOwner() { libssh2_sftp_shutdown(sftp_); } SftpOwner(const SftpOwner&) = delete; SftpOwner& operator=(const SftpOwner&) = delete; [[nodiscard]] LIBSSH2_SFTP* get() const { return sftp_; } private: std::shared_ptr ssh_session_; LIBSSH2_SFTP* sftp_; }; class SftpHandle { public: SftpHandle(std::shared_ptr sftp, LIBSSH2_SFTP_HANDLE* handle) : sftp_(std::move(sftp)), handle_(handle) {} ~SftpHandle() { libssh2_sftp_close_handle(handle_); } SftpHandle(const SftpHandle&) = delete; SftpHandle& operator=(const SftpHandle&) = delete; protected: [[nodiscard]] LIBSSH2_SFTP_HANDLE* get() const { return handle_; } private: std::shared_ptr sftp_; LIBSSH2_SFTP_HANDLE* handle_; }; jni::LocalRef CreateDirEntry(JNIEnv* env, const char* name, const LIBSSH2_SFTP_ATTRIBUTES& attrs); class Dir : SftpHandle { public: Dir(std::shared_ptr sftp, LIBSSH2_SFTP_HANDLE *handle) : SftpHandle(std::move(sftp), handle) {} jni::LocalRef List(JNIEnv* env, const jni::Ref& entry_clazz) { std::vector> tmp; size_t bufsize = 1024; auto buf = std::make_unique(bufsize); LIBSSH2_SFTP_ATTRIBUTES attrs; while (true) { auto size = bufsize / 2; auto ret = libssh2_sftp_readdir_ex(get(), buf.get(), size, buf.get() + size, size, &attrs); if (ret == 0) break; if (ret > 0) { // Skip . and .. entries. if (buf[0] == '.' && (ret == 1 || (buf[1] == '.' && ret == 2))) continue; auto obj = CreateDirEntry(env, buf.get(), attrs); if (obj) tmp.push_back(std::move(obj)); } else if (ret == LIBSSH2_ERROR_BUFFER_TOO_SMALL) { auto newsize = bufsize * 2; // Protect against overflow if (newsize <= bufsize) break; buf = std::make_unique(newsize); } else { // Error break; } } return jni::CreateArray(env, entry_clazz, std::move(tmp)); } }; class File : SftpHandle { public: File(std::shared_ptr sftp, LIBSSH2_SFTP_HANDLE *handle) : SftpHandle(std::move(sftp), handle) {} int32_t Read(uint8_t* data, int32_t size) { if (size <= 0) return 0; return libssh2_sftp_read(get(), reinterpret_cast(data), size); } void Seek(int64_t offset) { libssh2_sftp_seek64(get(), offset); } int32_t Write(const uint8_t* data, int32_t size) { if (size <= 0) return 0; return libssh2_sftp_write(get(), reinterpret_cast(data), size); } }; class SftpSession { public: explicit SftpSession(std::shared_ptr sftp) : sftp_(std::move(sftp)) {} ~SftpSession() = default; SftpSession(const SftpSession &) = delete; SftpSession &operator=(const SftpSession &) = delete; std::string GetLastError() { switch (libssh2_sftp_last_error(sftp_->get())) { case LIBSSH2_FX_OK: return ""; case LIBSSH2_FX_EOF: return "End of file"; case LIBSSH2_FX_NO_SUCH_FILE: return "No such file"; case LIBSSH2_FX_PERMISSION_DENIED: return "Permission denied"; case LIBSSH2_FX_FAILURE: return "Failure"; case LIBSSH2_FX_BAD_MESSAGE: return "Bad message"; case LIBSSH2_FX_NO_CONNECTION: return "No connection"; case LIBSSH2_FX_CONNECTION_LOST: return "Connection lost"; case LIBSSH2_FX_OP_UNSUPPORTED: return "Operation unsupported"; case LIBSSH2_FX_INVALID_HANDLE: return "Invalid handle"; case LIBSSH2_FX_NO_SUCH_PATH: return "No such path"; case LIBSSH2_FX_FILE_ALREADY_EXISTS: return "File already exists"; case LIBSSH2_FX_WRITE_PROTECT: return "Write protected"; case LIBSSH2_FX_NO_MEDIA: return "No media"; case LIBSSH2_FX_NO_SPACE_ON_FILESYSTEM: return "No space on filesystem"; case LIBSSH2_FX_QUOTA_EXCEEDED: return "Quota exceeded"; case LIBSSH2_FX_UNKNOWN_PRINCIPAL: return "Unknown principal"; case LIBSSH2_FX_LOCK_CONFLICT: return "Lock conflict"; case LIBSSH2_FX_DIR_NOT_EMPTY: return "Directory is not empty"; case LIBSSH2_FX_NOT_A_DIRECTORY: return "Not a directory"; case LIBSSH2_FX_INVALID_FILENAME: return "Invalid filename"; case LIBSSH2_FX_LINK_LOOP: return "Link loop"; default: return "Unknown error"; } } std::unique_ptr OpenDir(const std::string &path) { auto* handle = libssh2_sftp_open_ex(sftp_->get(), path.data(), path.size(), LIBSSH2_FXF_READ, 0700, LIBSSH2_SFTP_OPENDIR); if (!handle) return nullptr; return std::make_unique(sftp_, handle); } bool MakeDir(const std::string &path, int32_t mode) { return libssh2_sftp_mkdir_ex(sftp_->get(), path.data(), path.size(), mode) == 0; } bool RemoveDir(const std::string &path) { return libssh2_sftp_rmdir_ex(sftp_->get(), path.data(), path.size()) == 0; } std::unique_ptr OpenFile(const std::string &path, int32_t mode) { unsigned long flags = 0; switch (mode) { case 0: // READ(0), flags |= LIBSSH2_FXF_READ; break; case 1: // WRITE_CREATE_TRUNCATE(1), flags |= LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT | LIBSSH2_FXF_TRUNC; break; default: return nullptr; } auto* handle = libssh2_sftp_open_ex(sftp_->get(), path.data(), path.size(), flags, 0700, LIBSSH2_SFTP_OPENFILE); if (!handle) return nullptr; return std::make_unique(sftp_, handle); } bool Unlink(const std::string &path) { return libssh2_sftp_unlink_ex(sftp_->get(), path.data(), path.size()) == 0; } bool Symlink(const std::string &target, const std::string &path) { // The argument order does not seem to match the documentation, so this // might change? return libssh2_sftp_symlink_ex(sftp_->get(), target.data(), target.size(), const_cast(path.data()), path.size(), LIBSSH2_SFTP_SYMLINK) == 0; } std::optional Readlink(const std::string &path) { unsigned int bufsize = 8192; std::vector buf; while (true) { buf.resize(bufsize); auto ret = libssh2_sftp_symlink_ex(sftp_->get(), path.data(), path.size(), buf.data(), buf.size(), LIBSSH2_SFTP_READLINK); if (ret != LIBSSH2_ERROR_BUFFER_TOO_SMALL) { if (ret >= 0) return std::string(buf.data(), ret); return std::nullopt; } const auto previous = bufsize; bufsize *= 2; // Check for bufsize overflow. if (bufsize <= previous) return std::nullopt; } } jni::LocalRef Stat(JNIEnv* env, const std::string &path, bool follow_link) { LIBSSH2_SFTP_ATTRIBUTES attrs; auto ret = libssh2_sftp_stat_ex(sftp_->get(), path.data(), path.size(), follow_link ? LIBSSH2_SFTP_STAT : LIBSSH2_SFTP_LSTAT, &attrs); if (ret) return nullptr; return CreateDirEntry(env, path.c_str(), attrs); } private: std::shared_ptr sftp_; }; class unique_fd { public: constexpr unique_fd() : fd_(-1) {} explicit unique_fd(int fd) : fd_(fd) {} ~unique_fd() { if (fd_ != -1) close(fd_); } unique_fd(const unique_fd&) = delete; unique_fd& operator=(const unique_fd&) = delete; unique_fd(unique_fd&& other) noexcept : fd_(other.release()) {} unique_fd& operator=(unique_fd&& other) noexcept { reset(other.release()); return *this; } [[nodiscard]] int get() const { return fd_; } explicit operator bool() const { return fd_ != -1; } int release() { int fd = fd_; fd_ = -1; return fd; } void reset() { reset(-1); } void reset(int fd) { if (fd_ != -1) close(fd_); fd_ = fd; } private: int fd_; }; class SshSession { public: explicit SshSession(LIBSSH2_SESSION* session) : session_(session, SessionDeleter{}) { libssh2_session_set_blocking(session_.get(), 1); } ~SshSession() = default; SshSession(const SshSession&) = delete; SshSession& operator=(const SshSession&) = delete; std::string GetLastError() { char* ptr = nullptr; int len = 0; libssh2_session_last_error(session_.get(), &ptr, &len, /* want_buf */ 0); return {ptr, static_cast(len)}; } bool Connect(const std::string& host, int32_t port) { struct addrinfo hints{}; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; hints.ai_flags = AI_ADDRCONFIG; struct addrinfo* ptr = nullptr; char tmp[10]; snprintf(tmp, sizeof(tmp), "%d", static_cast(port)); int ret = getaddrinfo(host.c_str(), tmp, &hints, &ptr); if (ret) { libssh2_session_set_last_error(session_.get(), LIBSSH2_ERROR_SOCKET_NONE, gai_strerror(ret)); return false; } auto* i = ptr; for (; i; i = i->ai_next) { unique_fd fd(socket(i->ai_family, i->ai_socktype, i->ai_protocol)); if (fd) { if (connect(fd.get(), i->ai_addr, i->ai_addrlen) == 0) { fd_ = std::move(fd); freeaddrinfo(ptr); return true; } } libssh2_session_set_last_error(session_.get(), LIBSSH2_ERROR_SOCKET_NONE, strerror(errno)); } freeaddrinfo(ptr); return false; } std::optional> Handshake() { auto ret = libssh2_session_handshake(session_.get(), fd_.get()); if (ret) return std::nullopt; auto* fingerprint = libssh2_hostkey_hash(session_.get(), LIBSSH2_HOSTKEY_HASH_SHA256); if (!fingerprint) return std::nullopt; return std::vector(fingerprint, fingerprint + 32); } bool Authenticate(const std::string& username, const std::string& password) { return libssh2_userauth_password_ex(session_.get(), username.data(), username.size(), password.data(), password.size(), nullptr) == 0; } bool Authenticate(const std::string& username, const std::vector& public_key, const std::vector& private_key, const std::string& passphrase) { return libssh2_userauth_publickey_frommemory( session_.get(), username.data(), username.size(), reinterpret_cast(public_key.data()), public_key.size(), reinterpret_cast(private_key.data()), private_key.size(), passphrase.c_str()) == 0; } std::unique_ptr NewSftpSession() { auto* sftp_session = libssh2_sftp_init(session_.get()); if (!sftp_session) return nullptr; return std::make_unique(std::make_shared(session_, sftp_session)); } private: struct SessionDeleter { void operator()(LIBSSH2_SESSION* session) { if (session) { libssh2_session_disconnect(session, "Normal Shutdown"); libssh2_session_free(session); } } }; std::shared_ptr session_; unique_fd fd_; }; #pragma clang diagnostic push #pragma ide diagnostic ignored "MemoryLeak" jlong nativeSshSessionNew(JNIEnv*, jclass) { auto* session = libssh2_session_init(); if (!session) return 0; return reinterpret_cast(new SshSession(session)); } #pragma clang diagnostic pop void nativeSshSessionDestroy(JNIEnv*, jclass, jlong ptr) { delete reinterpret_cast(ptr); } jstring nativeSshSessionGetLastError(JNIEnv* env, jclass, jlong ptr) { return jni::UTF8ToString(env, reinterpret_cast(ptr)->GetLastError()).release(); } jboolean nativeSshSessionConnect(JNIEnv* env, jclass, jlong ptr, jstring host, jint port) { return reinterpret_cast(ptr)->Connect( jni::StringToUTF8(env, jni::ParamRef(env, host)), static_cast(port)) ? JNI_TRUE : JNI_FALSE; } jbyteArray nativeSshSessionHandshake(JNIEnv* env, jclass, jlong ptr) { auto fingerprint = reinterpret_cast(ptr)->Handshake(); if (fingerprint.has_value()) { return jni::VectorToByteArray(env, fingerprint.value()).release(); } return nullptr; } jboolean nativeSshSessionAuthenticate(JNIEnv* env, jclass, jlong ptr, jstring j_username, jstring password, jbyteArray public_key, jbyteArray private_key) { auto username = jni::StringToUTF8(env, jni::ParamRef(env, j_username)); if (public_key != nullptr && private_key != nullptr) { return reinterpret_cast(ptr)->Authenticate( username, jni::ByteArrayToVector(env, jni::ParamRef(env, public_key)), jni::ByteArrayToVector(env, jni::ParamRef(env, private_key)), password != nullptr ? jni::StringToUTF8(env, jni::ParamRef(env, password)) : "") ? JNI_TRUE : JNI_FALSE; } if (password != nullptr) { return reinterpret_cast(ptr)->Authenticate( username, jni::StringToUTF8(env, jni::ParamRef(env, password))) ? JNI_TRUE : JNI_FALSE; } return JNI_FALSE; } jlong nativeSshSessionNewSftpSession(JNIEnv*, jclass, jlong ptr) { return reinterpret_cast(reinterpret_cast(ptr)->NewSftpSession().release()); } void nativeSftpSessionDestroy(JNIEnv*, jclass, jlong ptr) { delete reinterpret_cast(ptr); } jstring nativeSftpSessionGetLastError(JNIEnv* env, jclass, jlong ptr) { return jni::UTF8ToString(env, reinterpret_cast(ptr)->GetLastError()).release(); } jlong nativeSftpSessionOpenDir(JNIEnv* env, jclass, jlong ptr, jstring path) { auto dir = reinterpret_cast(ptr)->OpenDir( jni::StringToUTF8(env, jni::ParamRef(env, path))); if (!dir) return 0L; return reinterpret_cast(dir.release()); } jboolean nativeSftpSessionMakeDir(JNIEnv* env, jclass, jlong ptr, jstring path, jint mode) { return reinterpret_cast(ptr)->MakeDir( jni::StringToUTF8(env, jni::ParamRef(env, path)), mode) ? JNI_TRUE : JNI_FALSE; } jboolean nativeSftpSessionRemoveDir(JNIEnv* env, jclass, jlong ptr, jstring path) { return reinterpret_cast(ptr)->RemoveDir( jni::StringToUTF8(env, jni::ParamRef(env, path))) ? JNI_TRUE : JNI_FALSE; } jlong nativeSftpSessionOpenFile(JNIEnv* env, jclass, jlong ptr, jstring path, jint mode) { auto file = reinterpret_cast(ptr)->OpenFile( jni::StringToUTF8(env, jni::ParamRef(env, path)), mode); if (!file) return 0L; return reinterpret_cast(file.release()); } jboolean nativeSftpSessionUnlink(JNIEnv* env, jclass, jlong ptr, jstring path) { return reinterpret_cast(ptr)->Unlink( jni::StringToUTF8(env, jni::ParamRef(env, path))) ? JNI_TRUE : JNI_FALSE; } jboolean nativeSftpSessionSymlink(JNIEnv* env, jclass, jlong ptr, jstring target, jstring path) { return reinterpret_cast(ptr)->Symlink( jni::StringToUTF8(env, jni::ParamRef(env, target)), jni::StringToUTF8(env, jni::ParamRef(env, path))) ? JNI_TRUE : JNI_FALSE; } jstring nativeSftpSessionReadlink(JNIEnv* env, jclass, jlong ptr, jstring path) { auto target = reinterpret_cast(ptr)->Readlink( jni::StringToUTF8(env, jni::ParamRef(env, path))); if (target.has_value()) return jni::UTF8ToString(env, target.value()).release(); return nullptr; } jobject nativeSftpSessionStat(JNIEnv* env, jclass, jlong ptr, jstring path, jboolean follow_link) { return reinterpret_cast(ptr)->Stat( env, jni::StringToUTF8(env, jni::ParamRef(env, path)), follow_link).release(); } void nativeDirDestroy(JNIEnv*, jclass, jlong ptr) { delete reinterpret_cast(ptr); } jni::GlobalRef g_DirEntryClass(nullptr, nullptr); jobjectArray nativeDirList(JNIEnv* env, jclass, jlong ptr) { return reinterpret_cast(ptr)->List(env, g_DirEntryClass).release(); } void nativeFileDestroy(JNIEnv*, jclass, jlong ptr) { delete reinterpret_cast(ptr); } jint nativeFileRead(JNIEnv* env, jclass, jlong ptr, jbyteArray array, jint offset, jint length) { jboolean is_copy = JNI_FALSE; bool critical = true; auto* data = reinterpret_cast(env->GetPrimitiveArrayCritical(array, &is_copy)); if (!data) { critical = false; data = env->GetByteArrayElements(array, &is_copy); if (!data) return -1; } auto ret = reinterpret_cast(ptr)->Read(reinterpret_cast(data + offset), length); if (critical) { env->ReleasePrimitiveArrayCritical(array, data, JNI_COMMIT); } else { env->ReleaseByteArrayElements(array, data, JNI_COMMIT); } return ret; } void nativeFileSeek(JNIEnv*, jclass, jlong ptr, jlong offset) { reinterpret_cast(ptr)->Seek(offset); } jint nativeFileWrite(JNIEnv* env, jclass, jlong ptr, jbyteArray array, jint offset, jint length) { jboolean is_copy = JNI_FALSE; bool critical = true; auto* data = reinterpret_cast(env->GetPrimitiveArrayCritical(array, &is_copy)); if (!data) { critical = false; data = env->GetByteArrayElements(array, &is_copy); if (!data) return -1; } auto ret = reinterpret_cast(ptr)->Write(reinterpret_cast(data + offset), length); if (critical) { env->ReleasePrimitiveArrayCritical(array, data, JNI_ABORT); } else { env->ReleaseByteArrayElements(array, data, JNI_ABORT); } return ret; } jni::GlobalRef g_NativeSftpClass(nullptr, nullptr); jmethodID g_CreateDirEntry; void RegisterSftp(JNIEnv* env) { auto clazz = jni::FindClass(env, "org/the_jk/cleversync/io/sftp/NativeSftp"); ABORT_IF_NULL(env, clazz); auto dir_entry_clazz = jni::FindClass(env, "org/the_jk/cleversync/io/sftp/NativeSftp$DirEntry"); ABORT_IF_NULL(env, dir_entry_clazz); static const JNINativeMethod methods[] = { { "nativeSshSessionNew", "()J", reinterpret_cast(&nativeSshSessionNew) }, { "nativeSshSessionDestroy", "(J)V", reinterpret_cast(&nativeSshSessionDestroy) }, { "nativeSshSessionGetLastError", "(J)Ljava/lang/String;", reinterpret_cast(&nativeSshSessionGetLastError) }, { "nativeSshSessionConnect", "(JLjava/lang/String;I)Z", reinterpret_cast(&nativeSshSessionConnect) }, { "nativeSshSessionHandshake", "(J)[B", reinterpret_cast(&nativeSshSessionHandshake) }, { "nativeSshSessionAuthenticate", "(JLjava/lang/String;Ljava/lang/String;[B[B)Z", reinterpret_cast(&nativeSshSessionAuthenticate) }, { "nativeSshSessionNewSftpSession", "(J)J", reinterpret_cast(&nativeSshSessionNewSftpSession) }, { "nativeSftpSessionDestroy", "(J)V", reinterpret_cast(&nativeSftpSessionDestroy) }, { "nativeSftpSessionGetLastError", "(J)Ljava/lang/String;", reinterpret_cast(&nativeSftpSessionGetLastError) }, { "nativeSftpSessionOpenDir", "(JLjava/lang/String;)J", reinterpret_cast(&nativeSftpSessionOpenDir) }, { "nativeSftpSessionMakeDir", "(JLjava/lang/String;I)Z", reinterpret_cast(&nativeSftpSessionMakeDir) }, { "nativeSftpSessionRemoveDir", "(JLjava/lang/String;)Z", reinterpret_cast(&nativeSftpSessionRemoveDir) }, { "nativeSftpSessionOpenFile", "(JLjava/lang/String;I)J", reinterpret_cast(&nativeSftpSessionOpenFile) }, { "nativeSftpSessionUnlink", "(JLjava/lang/String;)Z", reinterpret_cast(&nativeSftpSessionUnlink) }, { "nativeSftpSessionSymlink", "(JLjava/lang/String;Ljava/lang/String;)Z", reinterpret_cast(&nativeSftpSessionSymlink) }, { "nativeSftpSessionReadlink", "(JLjava/lang/String;)Ljava/lang/String;", reinterpret_cast(&nativeSftpSessionReadlink) }, { "nativeSftpSessionStat", "(JLjava/lang/String;Z)Lorg/the_jk/cleversync/io/sftp/NativeSftp$DirEntry;", reinterpret_cast(&nativeSftpSessionStat) }, { "nativeDirDestroy", "(J)V", reinterpret_cast(&nativeDirDestroy) }, { "nativeDirList", "(J)[Lorg/the_jk/cleversync/io/sftp/NativeSftp$DirEntry;", reinterpret_cast(&nativeDirList) }, { "nativeFileDestroy", "(J)V", reinterpret_cast(&nativeFileDestroy) }, { "nativeFileRead", "(J[BII)I", reinterpret_cast(&nativeFileRead) }, { "nativeFileSeek", "(JJ)V", reinterpret_cast(&nativeFileSeek) }, { "nativeFileWrite", "(J[BII)I", reinterpret_cast(&nativeFileWrite) }, }; auto ret = env->RegisterNatives(clazz.get(), methods, sizeof(methods) / sizeof(methods[0])); ABORT_IF_NOT_OK(ret); g_CreateDirEntry = env->GetStaticMethodID( clazz.get(), "createDirEntry", "(Ljava/lang/String;IJJ)Lorg/the_jk/cleversync/io/sftp/NativeSftp$DirEntry;"); ABORT_IF_NULL(env, g_CreateDirEntry); g_NativeSftpClass = clazz; g_DirEntryClass = dir_entry_clazz; } void UnregisterSftp() { g_CreateDirEntry = nullptr; g_NativeSftpClass.reset(); g_DirEntryClass.reset(); } jni::LocalRef CreateDirEntry(JNIEnv* env, const char* name, const LIBSSH2_SFTP_ATTRIBUTES& attrs) { auto j_name = jni::UTF8ToString(env, name); jlong size; jlong last_modified; jint type; if (attrs.flags & LIBSSH2_SFTP_ATTR_SIZE) { // Kotlin size casts Long to ULong size = static_cast(attrs.filesize); } else { size = 0L; } if (attrs.flags & LIBSSH2_SFTP_ATTR_ACMODTIME) { // Kotlin size casts Long to ULong last_modified = static_cast(attrs.mtime); } else { last_modified = 0L; } if (attrs.flags & LIBSSH2_SFTP_ATTR_PERMISSIONS) { switch (attrs.permissions & LIBSSH2_SFTP_S_IFMT) { case LIBSSH2_SFTP_S_IFREG: type = 1; break; case LIBSSH2_SFTP_S_IFDIR: type = 0; break; case LIBSSH2_SFTP_S_IFLNK: type = 2; break; case LIBSSH2_SFTP_S_IFIFO: case LIBSSH2_SFTP_S_IFCHR: case LIBSSH2_SFTP_S_IFBLK: case LIBSSH2_SFTP_S_IFSOCK: default: // Skip unknown or "weird" types return nullptr; } } else { // Skip unknown entries return nullptr; } return jni::CallStaticObjectMethod(env, g_NativeSftpClass, g_CreateDirEntry, j_name.get(), type, size, last_modified); } } // namespace jint JNI_OnLoad(JavaVM *vm, void *) { auto* env = jni::OnLoad(vm); // TODO: Check return libssh2_init(0); RegisterSftp(env); return jni::JNI_VERSION; } void JNI_OnUnload(JavaVM *, void *) { // Not called on Android (or in general), but if it where, this would be the place to unregister. UnregisterSftp(); libssh2_exit(); }